routes = new ArrayList<>();
+
+ routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
+
+ gatewayProperties.getRoutes().stream()
+ .filter(routeDefinition -> routes.contains(routeDefinition.getId()))
+ .forEach(routeDefinition -> routeDefinition.getPredicates().stream()
+ .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
+ .forEach(predicateDefinition ->
+ resources.add(
+ swaggerResource(routeDefinition.getId(),
+ predicateDefinition.
+ getArgs().
+ get(NameUtils.GENERATED_NAME_PREFIX+"0").
+ replace("/**", API_URL))))
+ );
+
+ return resources;
+ }
+
+ private SwaggerResource swaggerResource(String name, String location) {
+ SwaggerResource swaggerResource = new SwaggerResource();
+ swaggerResource.setName(name);
+ if (name.contains("reserve")) {
+ swaggerResource.setLocation(location.replace(API_URL, WEBFLUX_API_URL));
+ }else {
+ swaggerResource.setLocation(location);
+ }
+
+ swaggerResource.setSwaggerVersion("2.0");
+ return swaggerResource;
+ }
+}
diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java
new file mode 100644
index 0000000..5a78209
--- /dev/null
+++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java
@@ -0,0 +1,67 @@
+package org.egovframe.cloud.apigateway.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
+import org.springframework.security.web.server.authorization.AuthorizationContext;
+
+/**
+ * org.egovframe.cloud.apigateway.config.WebFluxSecurityConfig
+ *
+ * Spring Security Config 클래스
+ * ReactiveAuthorizationManager 구현체 ReactiveAuthorization 클래스를 통해 인증/인가 처리를 구현한다.
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/06/30
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/06/30 jaeyeolkim 최초 생성
+ *
+ */
+@EnableWebFluxSecurity // Spring Security 설정들을 활성화시켜 준다
+public class WebFluxSecurityConfig {
+
+ private final static String[] PERMITALL_ANTPATTERNS = {
+ ReactiveAuthorization.AUTHORIZATION_URI, "/", "/csrf",
+ "/user-service/login", "/?*-service/api/v1/messages/**", "/api/v1/messages/**",
+ "/?*-service/actuator/?*", "/actuator/?*",
+ "/?*-service/v2/api-docs", "/?*-service/v3/api-docs", "**/configuration/*", "/swagger*/**", "/webjars/**"
+ };
+ private final static String USER_JOIN_ANTPATTERNS = "/user-service/api/v1/users";
+
+ /**
+ * WebFlux 스프링 시큐리티 설정
+ *
+ * @see ReactiveAuthorization
+ * @param http
+ * @param check check(Mono authentication, AuthorizationContext context)
+ * @return
+ * @throws Exception
+ */
+ @Bean
+ public SecurityWebFilterChain configure(ServerHttpSecurity http, ReactiveAuthorizationManager check) throws Exception {
+ http
+ .csrf().disable()
+ .headers().frameOptions().disable()
+ .and()
+ .formLogin().disable()
+ .httpBasic().authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)) // login dialog disabled & 401 HttpStatus return
+ .and()
+ .authorizeExchange()
+ .pathMatchers(PERMITALL_ANTPATTERNS).permitAll()
+ .pathMatchers(HttpMethod.POST, USER_JOIN_ANTPATTERNS).permitAll()
+ .anyExchange().access(check);
+ return http.build();
+ }
+
+}
diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/ReactiveExceptionHandlerConfig.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/ReactiveExceptionHandlerConfig.java
new file mode 100644
index 0000000..4df9b1a
--- /dev/null
+++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/ReactiveExceptionHandlerConfig.java
@@ -0,0 +1,98 @@
+package org.egovframe.cloud.apigateway.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.egovframe.cloud.apigateway.exception.dto.ErrorCode;
+import org.springframework.boot.web.error.ErrorAttributeOptions;
+import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
+import org.springframework.boot.web.reactive.error.ErrorAttributes;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.web.reactive.function.server.ServerRequest;
+
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * org.egovframe.cloud.apigateway.exception.ReactiveExceptionHandlerConfig
+ *
+ * 에러 발생 시 에러 정보 중 필요한 내용만 반환한다
+ * ErrorCode 에서 status, code, message 세 가지 속성을 의존한다
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/07/16
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/16 jaeyeolkim 최초 생성
+ *
+ */
+@Slf4j
+//@Configuration
+public class ReactiveExceptionHandlerConfig {
+
+ private final MessageSource messageSource;
+
+ public ReactiveExceptionHandlerConfig(MessageSource messageSource) {
+ this.messageSource = messageSource;
+ }
+
+ /**
+ * 에러 발생 시 에러 정보 중 필요한 내용만 반환한다
+ *
+ * @return
+ */
+// @Bean
+ public ErrorAttributes errorAttributes() {
+ return new DefaultErrorAttributes() {
+ @Override
+ public Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
+ Map defaultMap = super.getErrorAttributes(request, options);
+ Map errorAttributes = new LinkedHashMap<>();
+
+ int status = (int) defaultMap.get("status");
+ ErrorCode errorCode = getErrorCode(status);
+ String message = messageSource.getMessage(errorCode.getMessage(), null, LocaleContextHolder.getLocale());
+ errorAttributes.put("timestamp", LocalDateTime.now());
+ errorAttributes.put("message", message);
+ errorAttributes.put("status", status);
+ errorAttributes.put("code", errorCode.getCode());
+ // API Gateway 에서 FieldError는 처리하지 않는다.
+
+ log.error("getErrorAttributes()={}", defaultMap);
+ return errorAttributes;
+ }
+ };
+ }
+
+ /**
+ * 상태코드로부터 ErrorCode 를 매핑하여 리턴한다.
+ *
+ * @param status
+ * @return
+ */
+ private ErrorCode getErrorCode(int status) {
+ switch (status) {
+ case 400:
+ return ErrorCode.ENTITY_NOT_FOUND;
+ case 401:
+ return ErrorCode.UNAUTHORIZED;
+ case 403:
+ return ErrorCode.ACCESS_DENIED;
+ case 404:
+ return ErrorCode.NOT_FOUND;
+ case 405:
+ return ErrorCode.METHOD_NOT_ALLOWED;
+ case 422:
+ return ErrorCode.UNPROCESSABLE_ENTITY;
+ default:
+ return ErrorCode.INTERNAL_SERVER_ERROR;
+ }
+ }
+}
diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/dto/ErrorCode.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/dto/ErrorCode.java
new file mode 100644
index 0000000..4da3263
--- /dev/null
+++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/dto/ErrorCode.java
@@ -0,0 +1,59 @@
+package org.egovframe.cloud.apigateway.exception.dto;
+
+/**
+ * org.egovframe.cloud.common.exception.dto.ErrorCode
+ *
+ * REST API 요청에 대한 오류 반환값을 정의
+ * ErrorResponse 클래스에서 status, code, message 세 가지 속성을 의존한다
+ * message 는 MessageSource 의 키 값을 정의하여 다국어 처리를 지원한다
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/07/16
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/16 jaeyeolkim 최초 생성
+ *
+ */
+public enum ErrorCode {
+
+ INVALID_INPUT_VALUE(400, "E001", "err.invalid.input.value"), // Bad Request
+ INVALID_TYPE_VALUE(400, "E002", "err.invalid.type.value"), // Bad Request
+ ENTITY_NOT_FOUND(400, "E003", "err.entity.not.found"), // Bad Request
+ UNAUTHORIZED(401, "E004", "err.unauthorized"), // The request requires an user authentication
+ ACCESS_DENIED(403, "E005", "err.access.denied"), // Forbidden, Access is Denied
+ NOT_FOUND(404, "E007", "err.not.found"), // Not found
+ METHOD_NOT_ALLOWED(405, "E008", "err.method.not.allowed"), // 요청 방법이 서버에 의해 알려졌으나, 사용 불가능한 상태
+ UNPROCESSABLE_ENTITY(422, "E009", "err.unprocessable.entity"), // Unprocessable Entity
+ INTERNAL_SERVER_ERROR(500, "E999", "err.internal.server"), // Server Error
+ SERVICE_UNAVAILABLE(503, "E010", "err.service.unavailable") // Service Unavailable
+ ;
+
+
+ private final int status;
+ private final String code;
+ private final String message;
+
+ ErrorCode(final int status, final String code, final String message) {
+ this.status = status;
+ this.code = code;
+ this.message = message;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+}
diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java
new file mode 100644
index 0000000..62dfd1c
--- /dev/null
+++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java
@@ -0,0 +1,49 @@
+package org.egovframe.cloud.apigateway.filter;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+@Component
+public class GlobalFilter extends AbstractGatewayFilterFactory {
+
+ public GlobalFilter() {
+ super(Config.class);
+ }
+
+ @Override
+ public GatewayFilter apply(Config config) {
+ // Pre filter
+ return ((exchange, chain) -> {
+ // Netty 비동기 방식 서버 사용시에는 ServerHttpRequest 를 사용해야 한다.
+ ServerHttpRequest request = exchange.getRequest();
+ ServerHttpResponse response = exchange.getResponse();
+
+ if (config.isPreLogger()) {
+ log.info("[GlobalFilter Start] request ID: {}, method: {}, path: {}", request.getId(), request.getMethod(), request.getPath());
+ }
+
+ // Post Filter
+ // 비동기 방식의 단일값 전달시 Mono 사용(Webflux)
+ return chain.filter(exchange).then(Mono.fromRunnable(() -> {
+ if (config.isPostLogger()) {
+ log.info("[GlobalFilter End ] request ID: {}, method: {}, path: {}, statusCode: {}", request.getId(), request.getMethod(), request.getPath(), response.getStatusCode());
+ }
+ }));
+ });
+ }
+
+ @Data
+ public static class Config {
+ // put the configure
+ private String baseMessage;
+ private boolean preLogger;
+ private boolean postLogger;
+ }
+}
diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/SwaggerHeaderFilter.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/SwaggerHeaderFilter.java
new file mode 100644
index 0000000..cb6bc9e
--- /dev/null
+++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/SwaggerHeaderFilter.java
@@ -0,0 +1,49 @@
+package org.egovframe.cloud.apigateway.filter;
+
+import org.egovframe.cloud.apigateway.config.SwaggerProvider;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * org.egovframe.cloud.apigateway.filter.SwaggerHeaderFilter
+ *
+ * Swagger header filter class
+ * 각 서비스 명을 붙여서 호출 할 수 있도록 filter를 추가 한다.
+ *
+ * @author 표준프레임워크센터 shinmj
+ * @version 1.0
+ * @since 2021/07/07
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/07 shinmj 최초 생성
+ *
+ */
+@Component
+public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
+ private static final String HEADER_NAME = "X-Forwarded-Prefix";
+
+ @Override
+ public GatewayFilter apply(Object config) {
+ return (exchange, chain) -> {
+ ServerHttpRequest request = exchange.getRequest();
+ String path = request.getURI().getPath();
+ if (!StringUtils.endsWithIgnoreCase(path, SwaggerProvider.API_URL)) {
+ return chain.filter(exchange);
+ }
+
+ String basePath = path.substring(0, path.lastIndexOf(SwaggerProvider.API_URL));
+ ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
+ ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
+
+ return chain.filter(newExchange);
+ };
+ }
+}
diff --git a/backend/apigateway/src/main/resources/application.yml b/backend/apigateway/src/main/resources/application.yml
new file mode 100644
index 0000000..342185a
--- /dev/null
+++ b/backend/apigateway/src/main/resources/application.yml
@@ -0,0 +1,67 @@
+server:
+ port: 8000
+
+spring:
+ application:
+ name: apigateway
+ cloud:
+ gateway:
+ routes:
+ - id: user-service
+ uri: lb://USER-SERVICE
+ predicates:
+ - Path=/user-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/user-service/(?.*), /$\{segment}
+ - SwaggerHeaderFilter
+ - id: portal-service
+ uri: lb://PORTAL-SERVICE
+ predicates:
+ - Path=/portal-service/**
+ filters:
+ - RewritePath=/portal-service/(?.*), /$\{segment}
+ - SwaggerHeaderFilter
+ - id: board-service
+ uri: lb://BOARD-SERVICE
+ predicates:
+ - Path=/board-service/**
+ filters:
+ - RewritePath=/board-service/(?.*), /$\{segment}
+ - SwaggerHeaderFilter
+ - id: reserve-item-service
+ uri: lb://RESERVE-ITEM-SERVICE
+ predicates:
+ - Path=/reserve-item-service/**
+ filters:
+ - RewritePath=/reserve-item-service/(?.*), /$\{segment}
+ - SwaggerHeaderFilter
+ - id: reserve-check-service
+ uri: lb://RESERVE-CHECK-SERVICE
+ predicates:
+ - Path=/reserve-check-service/**
+ filters:
+ - RewritePath=/reserve-check-service/(?.*), /$\{segment}
+ - SwaggerHeaderFilter
+ - id: reserve-request-service
+ uri: lb://RESERVE-REQUEST-SERVICE
+ predicates:
+ - Path=/reserve-request-service/**
+ filters:
+ - RewritePath=/reserve-request-service/(?.*), /$\{segment}
+ - SwaggerHeaderFilter
+ default-filters:
+ - name: GlobalFilter
+ args:
+ preLogger: true
+ postLogger: true
+ discovery:
+ locator:
+ enabled: true
+
+# config server actuator
+management:
+ endpoints:
+ web:
+ exposure:
+ include: refresh, health, beans
diff --git a/backend/apigateway/src/main/resources/bootstrap.yml b/backend/apigateway/src/main/resources/bootstrap.yml
new file mode 100644
index 0000000..6bf9cc8
--- /dev/null
+++ b/backend/apigateway/src/main/resources/bootstrap.yml
@@ -0,0 +1,5 @@
+spring:
+ cloud:
+ config:
+ uri: http://localhost:8888
+ name: apigateway
diff --git a/backend/apigateway/src/main/resources/logback-spring.xml b/backend/apigateway/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..03aa0f4
--- /dev/null
+++ b/backend/apigateway/src/main/resources/logback-spring.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${destination}
+
+ {"app.name":"${app_name}"}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/ApigatewayApplicationTests.java b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/ApigatewayApplicationTests.java
new file mode 100644
index 0000000..a211bac
--- /dev/null
+++ b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/ApigatewayApplicationTests.java
@@ -0,0 +1,13 @@
+package org.egovframe.cloud.apigateway;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class ApigatewayApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/MessageSourceConfigTest.java b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/MessageSourceConfigTest.java
new file mode 100644
index 0000000..b95c6e9
--- /dev/null
+++ b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/MessageSourceConfigTest.java
@@ -0,0 +1,25 @@
+package org.egovframe.cloud.apigateway.config;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+@SpringBootTest(webEnvironment = RANDOM_PORT)
+class MessageSourceConfigTest {
+
+ @Autowired
+ TestRestTemplate restTemplate;
+
+ @Test
+ public void 메세지를_외부위치에서_읽어온다() throws Exception {
+ // when
+ String message = restTemplate.getForObject("http://localhost:8000/api/v1/messages/common.login/ko", String.class);
+
+ // then
+ assertThat(message).isEqualTo("로그인");
+ }
+}
\ No newline at end of file
diff --git a/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorizationTest.java b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorizationTest.java
new file mode 100644
index 0000000..e2be571
--- /dev/null
+++ b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorizationTest.java
@@ -0,0 +1,28 @@
+package org.egovframe.cloud.apigateway.config;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+@SpringBootTest
+class ReactiveAuthorizationTest {
+
+ @Value("${server.port}")
+ private String PORT;
+
+ @Test
+ public void API요청시_토큰인증_만료된다() throws Exception {
+ // given
+ String baseUrl = "http://localhost:" + PORT;
+ String notValidToken = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI2NWEwMGY2NS04NDYwLTQ5YWYtOThlYy0wNDI5NzdlNTZmNGIiLCJhdXRob3JpdGllcyI6IlJPTEVfVVNFUiIsImV4cCI6MTYyNjc4MjQ0N30.qiScvtr1m88SHPLpHqcJiklXFyIQ7WBJdiFcdcb2B8YSWC59QcdRRgMtXDGSZnjBgF194W-GRBpHUta6VCkrfQ";
+
+ // when, then
+ WebTestClient.bindToServer().baseUrl(baseUrl).defaultHeader(HttpHeaders.AUTHORIZATION, notValidToken).build()
+ .get()
+ .uri("/user-service/api/v1/users")
+ .exchange().expectStatus().isUnauthorized()
+ ;
+ }
+}
\ No newline at end of file
diff --git a/backend/board-service/Dockerfile b/backend/board-service/Dockerfile
new file mode 100644
index 0000000..bd86ccf
--- /dev/null
+++ b/backend/board-service/Dockerfile
@@ -0,0 +1,15 @@
+# openjdk8 base image
+FROM openjdk:8-jre-alpine
+
+# config server uri: dockder run --e 로 변경 가능
+ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org
+# jar 파일이 복사되는 위치
+ENV APP_HOME=/usr/app/
+# 작업 시작 위치
+WORKDIR $APP_HOME
+# jar 파일 복사
+COPY build/libs/*.jar app.jar
+# application port
+#EXPOSE 8000
+# 실행 (application-cf.yml 프로필이 기본값)
+CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "app.jar"]
diff --git a/backend/board-service/build.gradle b/backend/board-service/build.gradle
new file mode 100644
index 0000000..09f9bf7
--- /dev/null
+++ b/backend/board-service/build.gradle
@@ -0,0 +1,98 @@
+plugins {
+ id 'org.springframework.boot' version '2.4.5'
+ id 'io.spring.dependency-management' version '1.0.11.RELEASE'
+ // querydsl
+ id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
+ id 'java'
+}
+
+group = 'org.egovframe.cloud'
+version = '0.1'
+sourceCompatibility = '1.8'
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+ maven { url "https://maven.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소
+}
+
+ext {
+ set('springCloudVersion', "2020.0.3")
+}
+
+dependencies {
+// implementation files('../../module-common/build/libs/module-common-0.1.jar') // 공통 모듈, @ComponentScan(basePackages={"org.egovframe.cloud"}) 추가해야 적용된다
+ implementation 'org.egovframe.cloud:module-common:0.1'
+ implementation('org.egovframe.rte:org.egovframe.rte.fdl.cmmn:4.0.0') {
+ exclude group: 'org.egovframe.rte', module: 'org.egovframe.rte.fdl.logging'
+ }
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ implementation 'org.springframework.cloud:spring-cloud-starter-config' // config
+ implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config
+ implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus
+ implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'
+ implementation 'org.springframework.cloud:spring-cloud-sleuth-zipkin'
+ implementation 'net.logstash.logback:logstash-logback-encoder:6.6' // logstash logback
+ implementation 'mysql:mysql-connector-java'
+ implementation 'io.jsonwebtoken:jjwt:0.9.1'
+ // querydsl
+ implementation 'com.querydsl:querydsl-jpa'
+ implementation 'com.querydsl:querydsl-sql:4.4.0'
+ implementation 'com.querydsl:querydsl-sql-spring:4.4.0'
+
+ //messaging
+ implementation 'org.springframework.cloud:spring-cloud-stream'
+ implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit'
+
+ // swagger
+ implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
+ implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
+ annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
+
+ // lombok
+ implementation 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.projectlombok:lombok'
+ testAnnotationProcessor 'org.projectlombok:lombok'
+
+ testImplementation 'com.h2database:h2'
+ testImplementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' // 테스트시에만 출력
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
+
+test {
+ useJUnitPlatform()
+}
+
+// querydsl 추가 시작
+def querydslDir = "$buildDir/generated/querydsl"
+querydsl {
+ jpa = true
+ querydslSourcesDir = querydslDir
+}
+sourceSets {
+ main.java.srcDir querydslDir
+}
+configurations {
+ querydsl.extendsFrom compileClasspath
+}
+compileQuerydsl {
+ options.annotationProcessorPath = configurations.querydsl
+}
+// querydsl 추가 끝
diff --git a/backend/board-service/gradlew b/backend/board-service/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/backend/board-service/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/backend/board-service/gradlew.bat b/backend/board-service/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/backend/board-service/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/backend/board-service/manifest.yml b/backend/board-service/manifest.yml
new file mode 100644
index 0000000..9897f1d
--- /dev/null
+++ b/backend/board-service/manifest.yml
@@ -0,0 +1,16 @@
+---
+applications:
+ - name: egov-board-service # CF push 시 생성되는 이름
+# memory: 512M # 메모리
+ instances: 1 # 인스턴스 수
+ host: egov-board-service # host 명으로 유일해야 함
+ path: build/libs/board-service-0.1.jar # build 후 생성된 jar 위치
+ buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인
+ services:
+ - egov-discovery-provided-service # discovery service binding
+ env:
+ spring_profiles_active: cf
+ spring_cloud_config_uri: https://egov-config.paas-ta.org
+ app_name: egov-board-service # logstash custom app name
+ TZ: Asia/Seoul
+ JAVA_OPTS: -Xss349k
diff --git a/backend/board-service/settings.gradle b/backend/board-service/settings.gradle
new file mode 100644
index 0000000..850e63b
--- /dev/null
+++ b/backend/board-service/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'board-service'
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/BoardServiceApplication.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/BoardServiceApplication.java
new file mode 100644
index 0000000..5a5fcc6
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/BoardServiceApplication.java
@@ -0,0 +1,39 @@
+package org.egovframe.cloud.boardservice;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.annotation.ComponentScan;
+
+/**
+ * org.egovframe.cloud.boardservice.BoardServiceApplication
+ *
+ * 게시판 서비스 어플리케이션 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@ComponentScan(basePackages={"org.egovframe.cloud.common", "org.egovframe.cloud.servlet", "org.egovframe.cloud.boardservice"}) // org.egovframe.cloud.common package 포함하기 위해
+@EntityScan({"org.egovframe.cloud.servlet.domain", "org.egovframe.cloud.boardservice.domain"})
+@SpringBootApplication
+public class BoardServiceApplication {
+
+ /**
+ * 메인
+ *
+ * @param args 매개변수
+ */
+ public static void main(String[] args) {
+ SpringApplication.run(BoardServiceApplication.class, args);
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/BoardApiController.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/BoardApiController.java
new file mode 100644
index 0000000..078cbbf
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/BoardApiController.java
@@ -0,0 +1,101 @@
+package org.egovframe.cloud.boardservice.api.board;
+
+import lombok.RequiredArgsConstructor;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardSaveRequestDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardUpdateRequestDto;
+import org.egovframe.cloud.boardservice.service.board.BoardService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.BoardApiController
+ *
+ * 게시판 Rest API 컨트롤러 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+@RestController
+public class BoardApiController {
+
+ /**
+ * 게시판 서비스
+ */
+ private final BoardService boardService;
+
+ /**
+ * 게시판 페이지 목록 조회
+ *
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시판 목록 응답 DTO
+ */
+ @GetMapping("/api/v1/boards")
+ public Page findPage(RequestDto requestDto,
+ @PageableDefault(sort = "board_no", direction = Sort.Direction.DESC) Pageable pageable) {
+ return boardService.findPage(requestDto, pageable);
+ }
+
+ /**
+ * 게시판 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @return BoardResponseDto 게시판 상세 응답 DTO
+ */
+ @GetMapping("/api/v1/boards/{boardNo}")
+ public BoardResponseDto findById(@PathVariable Integer boardNo) {
+ return boardService.findById(boardNo);
+ }
+
+ /**
+ * 게시판 등록
+ *
+ * @param requestDto 게시판 등록 요청 DTO
+ * @return BoardResponseDto 게시판 상세 응답 DTO
+ */
+ @PostMapping("/api/v1/boards")
+ public BoardResponseDto save(@RequestBody @Valid BoardSaveRequestDto requestDto) {
+ return boardService.save(requestDto);
+ }
+
+ /**
+ * 게시판 수정
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 게시판 수정 요청 DTO
+ * @return BoardResponseDto 게시판 상세 응답 DTO
+ */
+ @PutMapping("/api/v1/boards/{boardNo}")
+ public BoardResponseDto update(@PathVariable Integer boardNo, @RequestBody @Valid BoardUpdateRequestDto requestDto) {
+ return boardService.update(boardNo, requestDto);
+ }
+
+ /**
+ * 게시판 삭제
+ *
+ * @param boardNo 게시판 번호
+ */
+ @DeleteMapping("/api/v1/boards/{boardNo}")
+ public void delete(@PathVariable Integer boardNo) {
+ boardService.delete(boardNo);
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardListResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardListResponseDto.java
new file mode 100644
index 0000000..164bbdf
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardListResponseDto.java
@@ -0,0 +1,93 @@
+package org.egovframe.cloud.boardservice.api.board.dto;
+
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto
+ *
+ * 게시판 목록 응답 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class BoardListResponseDto implements Serializable {
+
+ /**
+ * SerialVersionUID
+ */
+ private static final long serialVersionUID = 115181553377528728L;
+
+ /**
+ * 게시판 번호
+ */
+ private Integer boardNo;
+
+ /**
+ * 게시판 명
+ */
+ private String boardName;
+
+ /**
+ * 스킨 유형 코드
+ */
+ private String skinTypeCode;
+
+ /**
+ * 스킨 유형 코드 명
+ */
+ private String skinTypeCodeName;
+
+ /**
+ * 생성 일시
+ */
+ private LocalDateTime createdDate;
+
+ /**
+ * 게시판 목록 응답 DTO 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param boardName 게시판 명
+ * @param skinTypeCode 스킨 유형 코드
+ */
+ @QueryProjection
+ public BoardListResponseDto(Integer boardNo, String boardName, String skinTypeCode) {
+ this.boardNo = boardNo;
+ this.boardName = boardName;
+ this.skinTypeCode = skinTypeCode;
+ }
+
+ /**
+ * 게시판 목록 응답 DTO 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param boardName 게시판 명
+ * @param skinTypeCode 스킨 유형 코드
+ * @param skinTypeCodeName 스킨 유형 코드 명
+ * @param createdDate 생성 일시
+ */
+ @QueryProjection
+ public BoardListResponseDto(Integer boardNo, String boardName, String skinTypeCode, String skinTypeCodeName, LocalDateTime createdDate) {
+ this.boardNo = boardNo;
+ this.boardName = boardName;
+ this.skinTypeCode = skinTypeCode;
+ this.skinTypeCodeName = skinTypeCodeName;
+ this.createdDate = createdDate;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardResponseDto.java
new file mode 100644
index 0000000..a7746b9
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardResponseDto.java
@@ -0,0 +1,199 @@
+package org.egovframe.cloud.boardservice.api.board.dto;
+
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto
+ *
+ * 게시판 상세 응답 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class BoardResponseDto implements Serializable {
+
+ /**
+ * SerialVersionUID
+ */
+ private static final long serialVersionUID = -7139346671431363426L;
+
+ /**
+ * 게시판 번호
+ */
+ private Integer boardNo;
+
+ /**
+ * 게시판 제목
+ */
+ private String boardName;
+
+ /**
+ * 스킨 유형 코드
+ */
+ private String skinTypeCode;
+
+ /**
+ * 제목 표시 길이
+ */
+ private Integer titleDisplayLength;
+
+ /**
+ * 게시물 표시 수
+ */
+ private Integer postDisplayCount;
+
+ /**
+ * 페이지 표시 수
+ */
+ private Integer pageDisplayCount;
+
+ /**
+ * 표시 신규 수
+ */
+ private Integer newDisplayDayCount;
+
+ /**
+ * 에디터 사용 여부
+ */
+ private Boolean editorUseAt;
+
+ /**
+ * 사용자 작성 여부
+ */
+ private Boolean userWriteAt;
+
+ /**
+ * 댓글 사용 여부
+ */
+ private Boolean commentUseAt;
+
+ /**
+ * 업로드 사용 여부
+ */
+ private Boolean uploadUseAt;
+
+ /**
+ * 업로드 제한 수
+ */
+ private Integer uploadLimitCount;
+
+ /**
+ * 업로드 제한 크기
+ */
+ private BigDecimal uploadLimitSize;
+
+ /**
+ * 게시물 목록
+ */
+ private List posts;
+
+ /**
+ * 게시판 상세 응답 DTO 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param boardName 게시판 명
+ * @param skinTypeCode 스킨 유형 코드
+ * @param titleDisplayLength 제목 표시 길이
+ * @param postDisplayCount 게시물 표시 수
+ * @param pageDisplayCount 페이지 표시 수
+ * @param newDisplayDayCount 신규 표시 일 수
+ * @param editorUseAt 에디터 사용 여부
+ * @param userWriteAt 사용자 작성 여부
+ * @param commentUseAt 댓글 사용 여부
+ * @param uploadUseAt 업로드 사용 여부
+ * @param uploadLimitCount 업로드 제한 수
+ * @param uploadLimitSize 업로드 제한 크기
+ */
+ @QueryProjection
+ public BoardResponseDto(Integer boardNo, String boardName, String skinTypeCode, Integer titleDisplayLength,
+ Integer postDisplayCount, Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt,
+ Boolean userWriteAt, Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount,
+ BigDecimal uploadLimitSize) {
+ this.boardNo = boardNo;
+ this.boardName = boardName;
+ this.skinTypeCode = skinTypeCode;
+ this.titleDisplayLength = titleDisplayLength;
+ this.postDisplayCount = postDisplayCount;
+ this.pageDisplayCount = pageDisplayCount;
+ this.newDisplayDayCount = newDisplayDayCount;
+ this.editorUseAt = editorUseAt;
+ this.userWriteAt = userWriteAt;
+ this.commentUseAt = commentUseAt;
+ this.uploadUseAt = uploadUseAt;
+ this.uploadLimitCount = uploadLimitCount;
+ this.uploadLimitSize = uploadLimitSize;
+ }
+
+ /**
+ * 게시판 엔티티를 생성자로 주입 받아서 게시판 상세 응답 DTO 속성 값 세팅
+ *
+ * @param entity 게시판 엔티티
+ */
+ public BoardResponseDto(Board entity) {
+ this.boardNo = entity.getBoardNo();
+ this.boardName = entity.getBoardName();
+ this.skinTypeCode = entity.getSkinTypeCode();
+ this.titleDisplayLength = entity.getTitleDisplayLength();
+ this.postDisplayCount = entity.getPostDisplayCount();
+ this.pageDisplayCount = entity.getPageDisplayCount();
+ this.newDisplayDayCount = entity.getNewDisplayDayCount();
+ this.editorUseAt = entity.getEditorUseAt();
+ this.userWriteAt = entity.getUserWriteAt();
+ this.commentUseAt = entity.getCommentUseAt();
+ this.uploadUseAt = entity.getUploadUseAt();
+ this.uploadLimitCount = entity.getUploadLimitCount();
+ this.uploadLimitSize = entity.getUploadLimitSize();
+ }
+
+ /**
+ * 게시판 상세 응답 DTO 속성 값으로 게시판 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @return Board 게시판 엔티티
+ */
+ public Board toEntity() {
+ return Board.builder()
+ .boardNo(boardNo)
+ .boardName(boardName)
+ .skinTypeCode(skinTypeCode)
+ .titleDisplayLength(titleDisplayLength)
+ .postDisplayCount(postDisplayCount)
+ .pageDisplayCount(pageDisplayCount)
+ .newDisplayDayCount(newDisplayDayCount)
+ .editorUseAt(editorUseAt)
+ .userWriteAt(userWriteAt)
+ .commentUseAt(commentUseAt)
+ .uploadUseAt(uploadUseAt)
+ .uploadLimitCount(uploadLimitCount)
+ .uploadLimitSize(uploadLimitSize)
+ .build();
+ }
+
+ /**
+ * 최근 게시물 목록 세팅
+ *
+ * @param posts 게시물 목록
+ */
+ public void setNewestPosts(List posts) {
+ this.posts = posts;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardSaveRequestDto.java
new file mode 100644
index 0000000..afe45ab
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardSaveRequestDto.java
@@ -0,0 +1,160 @@
+package org.egovframe.cloud.boardservice.api.board.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.BoardSaveRequestDto
+ *
+ * 게시판 등록 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class BoardSaveRequestDto {
+
+ /**
+ * 게시판 제목
+ */
+ @NotBlank(message = "{board.board_name} {err.required}")
+ private String boardName;
+
+ /**
+ * 스킨 유형 코드
+ */
+ @NotBlank(message = "{board.skin_type_code} {err.required}")
+ private String skinTypeCode;
+
+ /**
+ * 제목 표시 길이
+ */
+ @NotNull(message = "{board.title_display_length} {err.required}")
+ private Integer titleDisplayLength;
+
+ /**
+ * 게시물 표시 수
+ */
+ @NotNull(message = "{board.post_display_count} {err.required}")
+ private Integer postDisplayCount;
+
+ /**
+ * 페이지 표시 수
+ */
+ @NotNull(message = "{board.page_display_count} {err.required}")
+ private Integer pageDisplayCount;
+
+ /**
+ * 표시 신규 수
+ */
+ @NotNull(message = "{board.new_display_day_count} {err.required}")
+ private Integer newDisplayDayCount;
+
+ /**
+ * 에디터 사용 여부
+ */
+ @NotNull(message = "{board.editor_use_at} {err.required}")
+ private Boolean editorUseAt;
+
+ /**
+ * 사용자 작성 여부
+ */
+ @NotNull(message = "{board.user_write_at} {err.required}")
+ private Boolean userWriteAt;
+
+ /**
+ * 댓글 사용 여부
+ */
+ @NotNull(message = "{board.comment_use_at} {err.required}")
+ private Boolean commentUseAt;
+
+ /**
+ * 업로드 사용 여부
+ */
+ @NotNull(message = "{board.upload_use_at} {err.required}")
+ private Boolean uploadUseAt;
+
+ /**
+ * 업로드 제한 수
+ */
+ private Integer uploadLimitCount;
+
+ /**
+ * 업로드 제한 크기
+ */
+ private BigDecimal uploadLimitSize;
+
+ /**
+ * 게시판 등록 요청 DTO 클래스 생성자
+ * 빌더 패턴으로 객체 생성
+ *
+ * @param boardName 게시판 명
+ * @param skinTypeCode 스킨 유형 코드
+ * @param titleDisplayLength 제목 표시 길이
+ * @param postDisplayCount 게시물 표시 수
+ * @param pageDisplayCount 페이지 표시 수
+ * @param newDisplayDayCount 신규 표시 일 수
+ * @param editorUseAt 에디터 사용 여부
+ * @param userWriteAt 사용자 작성 여부
+ * @param commentUseAt 댓글 사용 여부
+ * @param uploadUseAt 업로드 사용 여부
+ * @param uploadLimitCount 업로드 제한 수
+ * @param uploadLimitSize 업로드 제한 크기
+ */
+ @Builder
+ public BoardSaveRequestDto(String boardName, String skinTypeCode, Integer titleDisplayLength, Integer postDisplayCount,
+ Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, Boolean userWriteAt,
+ Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, BigDecimal uploadLimitSize) {
+ this.boardName = boardName;
+ this.skinTypeCode = skinTypeCode;
+ this.titleDisplayLength = titleDisplayLength;
+ this.postDisplayCount = postDisplayCount;
+ this.pageDisplayCount = pageDisplayCount;
+ this.newDisplayDayCount = newDisplayDayCount;
+ this.editorUseAt = editorUseAt;
+ this.userWriteAt = userWriteAt;
+ this.commentUseAt = commentUseAt;
+ this.uploadUseAt = uploadUseAt;
+ this.uploadLimitCount = uploadLimitCount;
+ this.uploadLimitSize = uploadLimitSize;
+ }
+
+ /**
+ * 게시판 등록 요청 DTO 속성 값으로 게시판 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @return Board 게시판 엔티티
+ */
+ public Board toEntity() {
+ return Board.builder()
+ .boardName(boardName)
+ .skinTypeCode(skinTypeCode)
+ .titleDisplayLength(titleDisplayLength)
+ .postDisplayCount(postDisplayCount)
+ .pageDisplayCount(pageDisplayCount)
+ .newDisplayDayCount(newDisplayDayCount)
+ .editorUseAt(editorUseAt)
+ .userWriteAt(userWriteAt)
+ .commentUseAt(commentUseAt)
+ .uploadUseAt(uploadUseAt)
+ .uploadLimitCount(uploadLimitCount)
+ .uploadLimitSize(uploadLimitSize)
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardUpdateRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardUpdateRequestDto.java
new file mode 100644
index 0000000..c75420c
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardUpdateRequestDto.java
@@ -0,0 +1,160 @@
+package org.egovframe.cloud.boardservice.api.board.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.BoardUpdateRequestDto
+ *
+ * 게시판 수정 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/08
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/08 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class BoardUpdateRequestDto {
+
+ /**
+ * 게시판 제목
+ */
+ @NotBlank(message = "{board.board_name} {err.required}")
+ private String boardName;
+
+ /**
+ * 스킨 유형 코드
+ */
+ @NotBlank(message = "{board.skin_type_code} {err.required}")
+ private String skinTypeCode;
+
+ /**
+ * 제목 표시 길이
+ */
+ @NotNull(message = "{board.title_display_length} {err.required}")
+ private Integer titleDisplayLength;
+
+ /**
+ * 게시물 표시 수
+ */
+ @NotNull(message = "{board.post_display_count} {err.required}")
+ private Integer postDisplayCount;
+
+ /**
+ * 페이지 표시 수
+ */
+ @NotNull(message = "{board.page_display_count} {err.required}")
+ private Integer pageDisplayCount;
+
+ /**
+ * 표시 신규 수
+ */
+ @NotNull(message = "{board.new_display_day_count} {err.required}")
+ private Integer newDisplayDayCount;
+
+ /**
+ * 에디터 사용 여부
+ */
+ @NotNull(message = "{board.editor_use_at} {err.required}")
+ private Boolean editorUseAt;
+
+ /**
+ * 사용자 작성 여부
+ */
+ @NotNull(message = "{board.user_write_at} {err.required}")
+ private Boolean userWriteAt;
+
+ /**
+ * 댓글 사용 여부
+ */
+ @NotNull(message = "{board.comment_use_at} {err.required}")
+ private Boolean commentUseAt;
+
+ /**
+ * 업로드 사용 여부
+ */
+ @NotNull(message = "{board.upload_use_at} {err.required}")
+ private Boolean uploadUseAt;
+
+ /**
+ * 업로드 제한 수
+ */
+ private Integer uploadLimitCount;
+
+ /**
+ * 업로드 제한 크기
+ */
+ private BigDecimal uploadLimitSize;
+
+ /**
+ * 게시판 등록 요청 DTO 클래스 생성자
+ * 빌더 패턴으로 객체 생성
+ *
+ * @param boardName 게시판 명
+ * @param skinTypeCode 스킨 유형 코드
+ * @param titleDisplayLength 제목 표시 길이
+ * @param postDisplayCount 게시물 표시 수
+ * @param pageDisplayCount 페이지 표시 수
+ * @param newDisplayDayCount 신규 표시 일 수
+ * @param editorUseAt 에디터 사용 여부
+ * @param userWriteAt 사용자 작성 여부
+ * @param commentUseAt 댓글 사용 여부
+ * @param uploadUseAt 업로드 사용 여부
+ * @param uploadLimitCount 업로드 제한 수
+ * @param uploadLimitSize 업로드 제한 크기
+ */
+ @Builder
+ public BoardUpdateRequestDto(String boardName, String skinTypeCode, Integer titleDisplayLength, Integer postDisplayCount,
+ Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, Boolean userWriteAt,
+ Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, BigDecimal uploadLimitSize) {
+ this.boardName = boardName;
+ this.skinTypeCode = skinTypeCode;
+ this.titleDisplayLength = titleDisplayLength;
+ this.postDisplayCount = postDisplayCount;
+ this.pageDisplayCount = pageDisplayCount;
+ this.newDisplayDayCount = newDisplayDayCount;
+ this.editorUseAt = editorUseAt;
+ this.userWriteAt = userWriteAt;
+ this.commentUseAt = commentUseAt;
+ this.uploadUseAt = uploadUseAt;
+ this.uploadLimitCount = uploadLimitCount;
+ this.uploadLimitSize = uploadLimitSize;
+ }
+
+ /**
+ * 게시판 등록 요청 DTO 속성 값으로 게시판 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @return Board 게시판 엔티티
+ */
+ public Board toEntity() {
+ return Board.builder()
+ .boardName(boardName)
+ .skinTypeCode(skinTypeCode)
+ .titleDisplayLength(titleDisplayLength)
+ .postDisplayCount(postDisplayCount)
+ .pageDisplayCount(pageDisplayCount)
+ .newDisplayDayCount(newDisplayDayCount)
+ .editorUseAt(editorUseAt)
+ .userWriteAt(userWriteAt)
+ .commentUseAt(commentUseAt)
+ .uploadUseAt(uploadUseAt)
+ .uploadLimitCount(uploadLimitCount)
+ .uploadLimitSize(uploadLimitSize)
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/CommentApiController.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/CommentApiController.java
new file mode 100644
index 0000000..6fc89f8
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/CommentApiController.java
@@ -0,0 +1,150 @@
+package org.egovframe.cloud.boardservice.api.comment;
+
+import lombok.RequiredArgsConstructor;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentUpdateRequestDto;
+import org.egovframe.cloud.boardservice.service.comment.CommentService;
+import org.springframework.data.domain.Pageable;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.Map;
+
+/**
+ * org.egovframe.cloud.commentservice.api.comment.CommentApiController
+ *
+ * 댓글 Rest API 컨트롤러 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+@RestController
+public class CommentApiController {
+
+ /**
+ * 댓글 서비스
+ */
+ private final CommentService commentService;
+
+ /**
+ * 게시글의 전체 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ @GetMapping("/api/v1/comments/total/{boardNo}/{postsNo}")
+ public Map findTotal(@PathVariable Integer boardNo, @PathVariable Integer postsNo) {
+ return commentService.findAll(boardNo, postsNo, null);
+ }
+
+ /**
+ * 게시글의 전체 미삭제 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ @GetMapping("/api/v1/comments/all/{boardNo}/{postsNo}")
+ public Map findAll(@PathVariable Integer boardNo, @PathVariable Integer postsNo) {
+ return commentService.findAll(boardNo, postsNo, 0);
+ }
+
+ /**
+ * 게시글의 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param pageable 페이지 정보
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ @GetMapping("/api/v1/comments/{boardNo}/{postsNo}")
+ public Map findPage(@PathVariable Integer boardNo, @PathVariable Integer postsNo, Pageable pageable) {
+ return commentService.findPage(boardNo, postsNo, null, pageable);
+ }
+
+ /**
+ * 게시글의 미삭제 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param pageable 페이지 정보
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ @GetMapping("/api/v1/comments/list/{boardNo}/{postsNo}")
+ public Map findListPage(@PathVariable Integer boardNo, @PathVariable Integer postsNo, Pageable pageable) {
+ return commentService.findPage(boardNo, postsNo, 0, pageable);
+ }
+
+ /**
+ * 댓글 등록
+ *
+ * @param requestDto 댓글 등록 요청 DTO
+ */
+ @PostMapping("/api/v1/comments")
+ public CommentResponseDto save(@RequestBody @Valid CommentSaveRequestDto requestDto) {
+ return commentService.save(requestDto);
+ }
+
+ /**
+ * 댓글 수정(작성자 체크)
+ *
+ * @param requestDto 게시물 수정 요청 DTO
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @PutMapping("/api/v1/comments/update")
+ public CommentResponseDto updateByCreator(@RequestBody @Valid CommentUpdateRequestDto requestDto) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+
+ return commentService.update(requestDto, userId);
+ }
+
+ /**
+ * 댓글 삭제(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ */
+ @DeleteMapping("/api/v1/comments/delete/{boardNo}/{postsNo}/{commentNo}")
+ public void deleteByCreator(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @PathVariable Integer commentNo) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+
+ commentService.delete(boardNo, postsNo, commentNo, userId);
+ }
+
+ /**
+ * 댓글 수정
+ *
+ * @param requestDto 댓글 수정 요청 DTO
+ */
+ @PutMapping("/api/v1/comments")
+ public CommentResponseDto update(@RequestBody @Valid CommentUpdateRequestDto requestDto) {
+ return commentService.update(requestDto);
+ }
+
+ /**
+ * 댓글 삭제
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ */
+ @DeleteMapping("/api/v1/comments/{boardNo}/{postsNo}/{commentNo}")
+ public void delete(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @PathVariable Integer commentNo) {
+ commentService.delete(boardNo, postsNo, commentNo);
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentListResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentListResponseDto.java
new file mode 100644
index 0000000..2bf301f
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentListResponseDto.java
@@ -0,0 +1,130 @@
+package org.egovframe.cloud.boardservice.api.comment.dto;
+
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto
+ *
+ * 댓글 목록 응답 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class CommentListResponseDto implements Serializable {
+
+ /**
+ * SerialVersionUID
+ */
+ private static final long serialVersionUID = -8163130888886378482L;
+
+ /**
+ * 게시판 번호
+ */
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ private Integer postsNo;
+
+ /**
+ * 댓글 번호
+ */
+ private Integer commentNo;
+
+ /**
+ * 댓글 내용
+ */
+ private String commentContent;
+
+ /**
+ * 그룹 번호
+ */
+ private Integer groupNo;
+
+ /**
+ * 부모 댓글 번호
+ */
+ private Integer parentCommentNo;
+
+ /**
+ * 깊이 순서
+ */
+ private Integer depthSeq;
+
+ /**
+ * 정렬 순서
+ */
+ private Integer sortSeq;
+
+ /**
+ * 삭제 여부
+ */
+ private Integer deleteAt;
+
+ /**
+ * 생성자 id
+ */
+ private String createdBy;
+
+ /**
+ * 생성자 명
+ */
+ private String createdName;
+
+ /**
+ * 생성 일시
+ */
+ private LocalDateTime createdDate;
+
+ /**
+ * 댓글 목록 응답 DTO 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ * @param commentContent 댓글 내용
+ * @param parentCommentNo 부모 댓글 번호
+ * @param depthSeq 깊이 순서
+ * @param sortSeq 정렬 순서
+ * @param deleteAt 삭제 여부
+ * @param createdBy 생성자 id
+ * @param createdName 생성자 명
+ * @param createdDate 생성 일시
+ */
+ @QueryProjection
+ public CommentListResponseDto(Integer boardNo, Integer postsNo, Integer commentNo,
+ String commentContent, Integer groupNo, Integer parentCommentNo,
+ Integer depthSeq, Integer sortSeq, Integer deleteAt,
+ String createdBy, String createdName, LocalDateTime createdDate) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ this.commentNo = commentNo;
+ this.commentContent = commentContent;
+ this.groupNo = groupNo;
+ this.parentCommentNo = parentCommentNo;
+ this.depthSeq = depthSeq;
+ this.sortSeq = sortSeq;
+ this.deleteAt = deleteAt;
+ this.createdBy = createdBy;
+ this.createdName = createdName;
+ this.createdDate = createdDate;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentResponseDto.java
new file mode 100644
index 0000000..d06ab66
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentResponseDto.java
@@ -0,0 +1,116 @@
+package org.egovframe.cloud.boardservice.api.comment.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.comment.Comment;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto
+ *
+ * 댓글 상세 응답 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/11
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/11 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class CommentResponseDto implements Serializable {
+
+ /**
+ * SerialVersionUID
+ */
+ private static final long serialVersionUID = -3087424580328463654L;
+
+ /**
+ * 게시판 번호
+ */
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ private Integer postsNo;
+
+ /**
+ * 댓글 번호
+ */
+ private Integer commentNo;
+
+ /**
+ * 댓글 내용
+ */
+ private String commentContent;
+
+ /**
+ * 그룹 번호
+ */
+ private Integer groupNo;
+
+ /**
+ * 부모 댓글 번호
+ */
+ private Integer parentCommentNo;
+
+ /**
+ * 깊이 순서
+ */
+ private Integer depthSeq;
+
+ /**
+ * 정렬 순서
+ */
+ private Integer sortSeq;
+
+ /**
+ * 삭제 여부
+ */
+ private Integer deleteAt;
+
+ /**
+ * 생성자 id
+ */
+ private String createdBy;
+
+ /**
+ * 생성자 명
+ */
+ private String createdName;
+
+ /**
+ * 생성 일시
+ */
+ private LocalDateTime createdDate;
+
+ /**
+ * 댓글 엔티티를 생성자로 주입 받아서 댓글 상세 응답 DTO 속성 값 세팅
+ *
+ * @param entity 댓글 엔티티
+ */
+ public CommentResponseDto(Comment entity) {
+ this.postsNo = entity.getCommentId().getPostsId().getPostsNo();
+ this.boardNo = entity.getCommentId().getPostsId().getBoardNo();
+ this.commentNo = entity.getCommentId().getCommentNo();
+ this.commentContent = entity.getCommentContent();
+ this.groupNo = entity.getGroupNo();
+ this.parentCommentNo = entity.getParentCommentNo();
+ this.depthSeq = entity.getDepthSeq();
+ this.sortSeq = entity.getSortSeq();
+ this.deleteAt = entity.getDeleteAt();
+ this.createdBy = entity.getCreatedBy();
+ this.createdName = entity.getCreator() != null ? entity.getCreator().getUserName() : null;
+ this.createdDate = entity.getCreatedDate();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentSaveRequestDto.java
new file mode 100644
index 0000000..31b469f
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentSaveRequestDto.java
@@ -0,0 +1,137 @@
+package org.egovframe.cloud.boardservice.api.comment.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.comment.Comment;
+import org.egovframe.cloud.boardservice.domain.comment.CommentId;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto
+ *
+ * 댓글 등록 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class CommentSaveRequestDto {
+
+ /**
+ * 게시판 번호
+ */
+ @NotNull(message = "{board.board_no} {err.required}")
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ @NotNull(message = "{posts.posts_no} {err.required}")
+ private Integer postsNo;
+
+ /**
+ * 댓글 내용
+ */
+ @NotBlank(message = "{comment.comment_content} {err.required}")
+ private String commentContent;
+
+ /**
+ * 그룹 번호
+ */
+ private Integer groupNo;
+
+ /**
+ * 부모 댓글 번호
+ */
+ private Integer parentCommentNo;
+
+ /**
+ * 깊이 순서
+ */
+ private Integer depthSeq;
+
+ /**
+ * 댓글 등록 요청 DTO 클래스 생성자
+ * 빌더 패턴으로 객체 생성
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentContent 댓글 내용
+ * @param groupNo 그룹 번호
+ * @param parentCommentNo 부모 댓글 번호
+ * @param depthSeq 깊이 순서
+ */
+ @Builder
+ public CommentSaveRequestDto(Integer boardNo, Integer postsNo, String commentContent, Integer groupNo, Integer parentCommentNo, Integer depthSeq) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ this.commentContent = commentContent;
+ this.groupNo = groupNo;
+ this.parentCommentNo = parentCommentNo;
+ this.depthSeq = depthSeq;
+ }
+
+ /**
+ * 댓글 등록 요청 DTO 속성 값으로 댓글 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @param commentNo 댓글 번호
+ * @param sortSeq 정렬 순서
+ * @return Comment 댓글 엔티티
+ */
+ public Comment toEntity(Integer commentNo, Integer sortSeq) {
+ return Comment.builder()
+ .commentId(CommentId.builder()
+ .postsId(PostsId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .build())
+ .commentNo(commentNo)
+ .build())
+ .commentContent(commentContent)
+ .groupNo(groupNo)
+ .parentCommentNo(parentCommentNo)
+ .depthSeq(depthSeq)
+ .sortSeq(sortSeq)
+ .build();
+ }
+
+ /**
+ * 댓글 등록 요청 DTO 속성 값으로 댓글 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @param posts 게시물 엔티티
+ * @param commentNo 댓글 번호
+ * @param groupNo 그룹 번호
+ * @param sortSeq 정렬 순서
+ * @return Comment 댓글 엔티티
+ */
+ public Comment toEntity(Posts posts, Integer commentNo, Integer groupNo, Integer sortSeq) {
+ return Comment.builder()
+ .posts(posts)
+ .commentId(CommentId.builder()
+ .postsId(posts.getPostsId())
+ .commentNo(commentNo)
+ .build())
+ .commentContent(commentContent)
+ .groupNo(groupNo)
+ .parentCommentNo(parentCommentNo)
+ .depthSeq(depthSeq)
+ .sortSeq(sortSeq)
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentUpdateRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentUpdateRequestDto.java
new file mode 100644
index 0000000..eaf0e34
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentUpdateRequestDto.java
@@ -0,0 +1,90 @@
+package org.egovframe.cloud.boardservice.api.comment.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.comment.Comment;
+import org.egovframe.cloud.boardservice.domain.comment.CommentId;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto
+ *
+ * 댓글 수정 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class CommentUpdateRequestDto {
+
+ /**
+ * 게시판 번호
+ */
+ @NotNull(message = "{board.board_no} {err.required}")
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ @NotNull(message = "{posts.posts_no} {err.required}")
+ private Integer postsNo;
+
+ /**
+ * 댓글 번호
+ */
+ @NotNull(message = "{comment.comment_no} {err.required}")
+ private Integer commentNo;
+
+ /**
+ * 댓글 내용
+ */
+ @NotBlank(message = "{comment.comment_content} {err.required}")
+ private String commentContent;
+
+ /**
+ * 댓글 수정 요청 DTO 클래스 생성자
+ * 빌더 패턴으로 객체 생성
+ *
+ * @param commentContent 댓글 내용
+ */
+ @Builder
+ public CommentUpdateRequestDto(Integer boardNo, Integer postsNo, Integer commentNo, String commentContent) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ this.commentNo = commentNo;
+ this.commentContent = commentContent;
+ }
+
+ /**
+ * 댓글 수정 요청 DTO 속성 값으로 댓글 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @return Comment 댓글 엔티티
+ */
+ public Comment toEntity() {
+ return Comment.builder()
+ .commentId(CommentId.builder()
+ .postsId(PostsId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .build())
+ .commentNo(commentNo)
+ .build())
+ .commentContent(commentContent)
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/PostsApiController.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/PostsApiController.java
new file mode 100644
index 0000000..01ffb0b
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/PostsApiController.java
@@ -0,0 +1,229 @@
+package org.egovframe.cloud.boardservice.api.posts;
+
+import lombok.RequiredArgsConstructor;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.*;
+import org.egovframe.cloud.boardservice.service.posts.PostsService;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.egovframe.cloud.common.util.LogUtil;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * org.egovframe.cloud.postservice.api.posts.PostsApiController
+ *
+ * 게시물 Rest API 컨트롤러 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+@RestController
+public class PostsApiController {
+
+ /**
+ * 게시물 서비스
+ */
+ private final PostsService postsService;
+
+ /**
+ * 게시물(삭제 포함) 페이지 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시물 목록 응답 DTO
+ */
+ @GetMapping("/api/v1/posts/{boardNo}")
+ public Page findPage(@PathVariable Integer boardNo,
+ RequestDto requestDto,
+ @SortDefault.SortDefaults({
+ @SortDefault(sort = "notice_at", direction = Sort.Direction.DESC),
+ @SortDefault(sort = "board_no", direction = Sort.Direction.ASC),
+ @SortDefault(sort = "posts_no", direction = Sort.Direction.DESC)
+ }) Pageable pageable) {
+ return postsService.findPage(boardNo, null, requestDto, pageable);
+ }
+
+ /**
+ * 게시물(삭제 제외) 페이지 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시물 목록 응답 DTO
+ */
+ @GetMapping("/api/v1/posts/list/{boardNo}")
+ public Page findListPage(@PathVariable Integer boardNo,
+ RequestDto requestDto,
+ @SortDefault.SortDefaults({
+ @SortDefault(sort = "notice_at", direction = Sort.Direction.DESC),
+ @SortDefault(sort = "board_no", direction = Sort.Direction.ASC),
+ @SortDefault(sort = "posts_no", direction = Sort.Direction.DESC)
+ }) Pageable pageable) {
+ return postsService.findPage(boardNo, 0, requestDto, pageable);
+ }
+
+ /**
+ * 최근 게시물이 포함된 게시판 목록 조회
+ *
+ * @param boardNos 게시판 번호 목록
+ * @param postsCount 게시물 수
+ * @return Map 최근 게시물이 포함된 게시판 상세 응답 DTO Map
+ */
+ @GetMapping("/api/v1/posts/newest/{boardNos}/{postsCount}")
+ public Map findNewest(@PathVariable List boardNos, @PathVariable Integer postsCount) {
+ return postsService.findNewest(boardNos, postsCount);
+ }
+
+ /**
+ * 게시물(삭제 포함) 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @GetMapping("/api/v1/posts/{boardNo}/{postsNo}")
+ public PostsResponseDto findById(@PathVariable Integer boardNo, @PathVariable Integer postsNo) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+ userId = "anonymousUser".equals(userId) ? null : userId;
+
+ return postsService.findById(boardNo, postsNo, null, userId, LogUtil.getUserIp());
+ }
+
+ /**
+ * 게시물(삭제 제외) 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @GetMapping("/api/v1/posts/view/{boardNo}/{postsNo}")
+ public PostsResponseDto findViewById(@PathVariable Integer boardNo, @PathVariable Integer postsNo, RequestDto requestDto) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+ userId = "anonymousUser".equals(userId) ? null : userId;
+ final Integer deleteAt = 0; // 미삭제 게시물만 조회
+
+ return postsService.findById(boardNo, postsNo, deleteAt, userId, LogUtil.getUserIp(), requestDto);
+ }
+
+ /**
+ * 게시물 등록(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 게시물 등록 요청 DTO
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @PostMapping("/api/v1/posts/save/{boardNo}")
+ public PostsResponseDto saveByCreator(@PathVariable Integer boardNo, @RequestBody @Valid PostsSimpleSaveRequestDto requestDto) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+ return postsService.save(boardNo, requestDto, userId);
+ }
+
+ /**
+ * 게시물 수정(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param requestDto 게시물 수정 요청 DTO
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @PutMapping("/api/v1/posts/update/{boardNo}/{postsNo}")
+ public PostsResponseDto updateByCreator(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @RequestBody @Valid PostsSimpleSaveRequestDto requestDto) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+
+ return postsService.update(boardNo, postsNo, requestDto, userId);
+ }
+
+ /**
+ * 게시물 삭제(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ */
+ @DeleteMapping("/api/v1/posts/remove/{boardNo}/{postsNo}")
+ public void deleteByCreator(@PathVariable Integer boardNo, @PathVariable Integer postsNo) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+
+ postsService.remove(boardNo, postsNo, userId);
+ }
+
+ /**
+ * 게시물 등록
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 게시물 등록 요청 DTO
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @PostMapping("/api/v1/posts/{boardNo}")
+ public PostsResponseDto save(@PathVariable Integer boardNo, @RequestBody @Valid PostsSaveRequestDto requestDto) {
+ return postsService.save(boardNo, requestDto);
+ }
+
+ /**
+ * 게시물 수정
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param requestDto 게시물 수정 요청 DTO
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @PutMapping("/api/v1/posts/{boardNo}/{postsNo}")
+ public PostsResponseDto update(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @RequestBody @Valid PostsUpdateRequestDto requestDto) {
+ return postsService.update(boardNo, postsNo, requestDto);
+ }
+
+ /**
+ * 게시물 다건 삭제
+ *
+ * @param requestDtoList 게시물 삭제 요청 DTO List
+ * @return Long 삭제 건수
+ */
+ @PutMapping("/api/v1/posts/remove")
+ public Long remove(@RequestBody @Valid List requestDtoList) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+
+ return postsService.remove(requestDtoList, userId);
+ }
+
+ /**
+ * 게시물 다건 복원
+ *
+ * @param requestDtoList 게시물 삭제 요청 DTO List
+ * @return Long 복원 건수
+ */
+ @PutMapping("/api/v1/posts/restore")
+ public Long restore(@RequestBody @Valid List requestDtoList) {
+ String userId = SecurityContextHolder.getContext().getAuthentication().getName();
+
+ return postsService.restore(requestDtoList, userId);
+ }
+
+ /**
+ * 게시물 다건 완전 삭제
+ *
+ * @param requestDtoList 게시물 삭제 요청 DTO List
+ */
+ @PutMapping("/api/v1/posts/delete")
+ public void delete(@RequestBody @Valid List requestDtoList) {
+ postsService.delete(requestDtoList);
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsDeleteRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsDeleteRequestDto.java
new file mode 100644
index 0000000..5b21800
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsDeleteRequestDto.java
@@ -0,0 +1,55 @@
+package org.egovframe.cloud.boardservice.api.posts.dto;
+
+import lombok.Getter;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * org.egovframe.cloud.boardservice.api.posts.dto.PostsDeleteRequestDto
+ *
+ * 게시물 삭제 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/29
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/29 jooho 최초 생성
+ *
+ */
+@Getter
+public class PostsDeleteRequestDto {
+
+ /**
+ * 게시판 번호
+ */
+ @NotBlank(message = "{board.board_no} {err.required}")
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ @NotBlank(message = "{posts.posts_no} {err.required}")
+ private Integer postsNo;
+
+ /**
+ * 게시물 삭제 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @return Posts 게시물 엔티티
+ */
+ public Posts toEntity() {
+ return Posts.builder()
+ .postsId(PostsId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .build())
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsListResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsListResponseDto.java
new file mode 100644
index 0000000..be7e416
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsListResponseDto.java
@@ -0,0 +1,137 @@
+package org.egovframe.cloud.boardservice.api.posts.dto;
+
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.PostsListResponseDto
+ *
+ * 게시물 목록 응답 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class PostsListResponseDto implements Serializable {
+
+ /**
+ * SerialVersionUID
+ */
+ private static final long serialVersionUID = 3316086575500238046L;
+
+ /**
+ * 게시판 번호
+ */
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ private Integer postsNo;
+
+ /**
+ * 게시물 제목
+ */
+ private String postsTitle;
+
+ /**
+ * 게시물 내용
+ */
+ private String postsContent;
+
+ /**
+ * 게시물 답변 내용
+ */
+ private String postsAnswerContent;
+
+ /**
+ * 조회 수
+ */
+ private Integer readCount;
+
+ /**
+ * 공지 여부
+ */
+ private Boolean noticeAt;
+
+ /**
+ * 삭제 여부
+ */
+ private Integer deleteAt;
+
+ /**
+ * 생성자 id
+ */
+ private String createdBy;
+
+ /**
+ * 생성자 명
+ */
+ private String createdName;
+
+ /**
+ * 생성 일시
+ */
+ private LocalDateTime createdDate;
+
+ /**
+ * 신규 여부
+ */
+ private Boolean isNew;
+
+ /**
+ * 댓글 수
+ */
+ private Long commentCount;
+
+ /**
+ * 게시물 목록 응답 DTO 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param postsAnswerContent 게시물 답변 내용
+ * @param readCount 조회 수
+ * @param noticeAt 공지 여부
+ * @param deleteAt 삭제 여부
+ * @param createdBy 생성자 id
+ * @param createdName 생성자 명
+ * @param createdDate 생성 일시
+ * @param commentCount 댓글 수
+ */
+ @QueryProjection
+ public PostsListResponseDto(Integer boardNo, Integer postsNo, String postsTitle, String postsContent,
+ String postsAnswerContent, Integer readCount, Boolean noticeAt, Integer deleteAt,
+ String createdBy, String createdName, LocalDateTime createdDate, Integer newDisplayDayCount,
+ Long commentCount) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.postsAnswerContent = postsAnswerContent;
+ this.readCount = readCount;
+ this.noticeAt = noticeAt;
+ this.deleteAt = deleteAt;
+ this.createdBy = createdBy;
+ this.createdName = createdName;
+ this.createdDate = createdDate;
+ this.isNew = createdDate.plusDays(newDisplayDayCount).compareTo(LocalDateTime.now()) >= 0;
+ this.commentCount = commentCount;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsResponseDto.java
new file mode 100644
index 0000000..90facef
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsResponseDto.java
@@ -0,0 +1,215 @@
+package org.egovframe.cloud.boardservice.api.posts.dto;
+
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.PostsResponseDto
+ *
+ * 게시물 상세 응답 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class PostsResponseDto implements Serializable {
+
+ /**
+ * SerialVersionUID
+ */
+ private static final long serialVersionUID = 8644170429040511387L;
+
+ /**
+ * 게시판 번호
+ */
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ private Integer postsNo;
+
+ /**
+ * 게시물 제목
+ */
+ private String postsTitle;
+
+ /**
+ * 게시물 내용
+ */
+ private String postsContent;
+
+ /**
+ * 게시물 답변 내용
+ */
+ private String postsAnswerContent;
+
+ /**
+ * 첨부파일 코드
+ */
+ private String attachmentCode;
+
+ /**
+ * 조회 수
+ */
+ private Integer readCount;
+
+ /**
+ * 공지 여부
+ */
+ private Boolean noticeAt;
+
+ /**
+ * 삭제 여부
+ */
+ private Integer deleteAt;
+
+ /**
+ * 생성자
+ */
+ private String createdBy;
+
+ /**
+ * 생성자 명
+ */
+ private String createdName;
+
+ /**
+ * 생성 일시
+ */
+ private LocalDateTime createdDate;
+
+ /**
+ * 게시판 응답 DTO
+ */
+ private BoardResponseDto board;
+
+ /**
+ * 신규 여부
+ */
+ private Boolean isNew;
+
+ /**
+ * 유저 게시물 조회 수
+ */
+ private Long userPostsReadCount;
+
+ /**
+ * 이전 게시물 응답 DTO List
+ */
+ private List prevPosts;
+
+ /**
+ * 다음 게시물 응답 DTO List
+ */
+ private List nextPosts;
+
+ /**
+ * 게시물 상세 응답 DTO 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param postsAnswerContent 게시물 답변 내용
+ * @param attachmentCode 첨부파일 코드
+ * @param readCount 조회 수
+ * @param noticeAt 공지 여부
+ * @param deleteAt 삭제 여부
+ * @param createdBy 생성자 id
+ * @param createdName 생성자 명
+ * @param createdDate 생성 일시
+ * @param board 게시판
+ * @param userPostsReadCount 유저 게시물 조회 수
+ */
+ @QueryProjection
+ public PostsResponseDto(Integer boardNo, Integer postsNo, String postsTitle, String postsContent,
+ String postsAnswerContent, String attachmentCode, Integer readCount, Boolean noticeAt,
+ Integer deleteAt, String createdBy, String createdName, LocalDateTime createdDate,
+ BoardResponseDto board, Long userPostsReadCount) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.postsAnswerContent = postsAnswerContent;
+ this.attachmentCode = attachmentCode;
+ this.readCount = readCount;
+ this.noticeAt = noticeAt;
+ this.deleteAt = deleteAt;
+ this.createdBy = createdBy;
+ this.createdName = createdName;
+ this.createdDate = createdDate;
+ this.board = board;
+ if (this.board.getNewDisplayDayCount() != null && this.board.getNewDisplayDayCount() > 0) {
+ this.isNew = createdDate.plusDays(this.board.getNewDisplayDayCount()).compareTo(LocalDateTime.now()) <= 0;
+ } else {
+ this.isNew = false;
+ }
+ this.userPostsReadCount = userPostsReadCount;
+ }
+
+ /**
+ * 게시물 엔티티를 생성자로 주입 받아서 게시물 상세 응답 DTO 속성 값 세팅
+ *
+ * @param entity 게시물 엔티티
+ */
+ public PostsResponseDto(Posts entity) {
+ this.postsNo = entity.getPostsId().getPostsNo();
+ this.boardNo = entity.getPostsId().getBoardNo();
+ this.postsTitle = entity.getPostsTitle();
+ this.postsContent = entity.getPostsContent();
+ this.postsAnswerContent = entity.getPostsAnswerContent();
+ this.attachmentCode = entity.getAttachmentCode();
+ this.readCount = entity.getReadCount();
+ this.noticeAt = entity.getNoticeAt();
+ this.deleteAt = entity.getDeleteAt();
+ this.createdBy = entity.getCreatedBy();
+ this.createdName = entity.getCreator() != null ? entity.getCreator().getUserName() : null;
+ this.createdDate = entity.getCreatedDate();
+ this.board = new BoardResponseDto(entity.getBoard());
+ if (this.board.getNewDisplayDayCount() != null) {
+ this.isNew = createdDate.plusDays(this.board.getNewDisplayDayCount()).compareTo(LocalDateTime.now()) <= 0;
+ } else {
+ this.isNew = false;
+ }
+ }
+
+ /**
+ * 조회 수 증가
+ */
+ public void increaseReadCount() {
+ this.readCount = this.readCount + 1;
+ }
+
+ /**
+ * 이전 게시물
+ */
+ public void setPrevPosts(List prevPosts) {
+ this.prevPosts = prevPosts;
+ }
+
+ /**
+ * 다음 게시물
+ */
+ public void setNextPosts(List nextPosts) {
+ this.nextPosts = nextPosts;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSaveRequestDto.java
new file mode 100644
index 0000000..84ca615
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSaveRequestDto.java
@@ -0,0 +1,104 @@
+package org.egovframe.cloud.boardservice.api.posts.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.PostsSaveRequestDto
+ *
+ * 게시물 등록 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class PostsSaveRequestDto {
+
+ /**
+ * 게시물 제목
+ */
+ @NotBlank(message = "{posts.posts_title} {err.required}")
+ private String postsTitle;
+
+ /**
+ * 게시물 내용
+ */
+ @NotBlank(message = "{posts.posts_content} {err.required}")
+ private String postsContent;
+
+ /**
+ * 게시물 답변 내용
+ */
+ private String postsAnswerContent;
+
+ /**
+ * 첨부파일 코드
+ */
+ private String attachmentCode;
+
+ /**
+ * 공지 여부
+ */
+ @NotNull(message = "{posts.notice_at} {err.required}")
+ private Boolean noticeAt;
+
+ /**
+ * 게시물 등록 요청 DTO 클래스 생성자
+ * 빌더 패턴으로 객체 생성
+ *
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param attachmentCode 첨부파일 코드
+ * @param noticeAt 공지 여부
+ */
+ @Builder
+ public PostsSaveRequestDto(String postsTitle, String postsContent, String postsAnswerContent, String attachmentCode, Boolean noticeAt) {
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.postsAnswerContent = postsAnswerContent;
+ this.attachmentCode = attachmentCode;
+ this.noticeAt = noticeAt;
+ }
+
+ /**
+ * 게시물 등록 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Posts 게시물 엔티티
+ */
+ public Posts toEntity(Integer boardNo, Integer postsNo) {
+ return Posts.builder()
+ .board(Board.builder()
+ .boardNo(boardNo)
+ .build())
+ .postsId(PostsId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .build())
+ .postsTitle(postsTitle)
+ .postsContent(postsContent)
+ .postsAnswerContent(postsAnswerContent)
+ .attachmentCode(attachmentCode)
+ .noticeAt(noticeAt)
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleResponseDto.java
new file mode 100644
index 0000000..f56e986
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleResponseDto.java
@@ -0,0 +1,104 @@
+package org.egovframe.cloud.boardservice.api.posts.dto;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+
+import com.querydsl.core.annotations.QueryProjection;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto
+ *
+ * 게시물 응답 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/09/03
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/09/03 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class PostsSimpleResponseDto implements Serializable {
+
+ /**
+ * SerialVersionUID
+ */
+ private static final long serialVersionUID = 6916914915364711614L;
+
+ /**
+ * 게시판 번호
+ */
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ private Integer postsNo;
+
+ /**
+ * 게시물 제목
+ */
+ private String postsTitle;
+
+ /**
+ * 게시물 내용
+ */
+ private String postsContent;
+
+ /**
+ * 생성 일시
+ */
+ private LocalDateTime createdDate;
+
+ /**
+ * 신규 여부
+ */
+ private Boolean isNew;
+
+ /**
+ * 게시물 응답 DTO 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param createdDate 생성 일시
+ */
+ @QueryProjection
+ public PostsSimpleResponseDto(Integer boardNo, Integer postsNo,
+ String postsTitle, String postsContent, LocalDateTime createdDate) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ this.postsTitle = postsTitle;
+ // this.postsContent = HtmlUtils.htmlEscape(HtmlUtils.htmlUnescape(postsContent)); // frontend 에서 처리
+ this.postsContent = postsContent;
+ this.createdDate = createdDate;
+ }
+
+ /**
+ * 신규 여부 계산
+ *
+ * @param boardResponseDto 게시판 상세 응답 DTO
+ * @return PostsSimpleResponseDto 게시물 응답 DTO
+ */
+ public PostsSimpleResponseDto setIsNew(BoardResponseDto boardResponseDto) {
+ if (boardResponseDto.getNewDisplayDayCount() != null) {
+ this.isNew = createdDate.plusDays(boardResponseDto.getNewDisplayDayCount()).compareTo(LocalDateTime.now()) <= 0;
+ } else {
+ this.isNew = false;
+ }
+ return this;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleSaveRequestDto.java
new file mode 100644
index 0000000..7ae2d3f
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleSaveRequestDto.java
@@ -0,0 +1,87 @@
+package org.egovframe.cloud.boardservice.api.posts.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleSaveRequestDto
+ *
+ * 게시물 등록 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/09/10
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/09/10 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class PostsSimpleSaveRequestDto {
+
+ /**
+ * 게시물 제목
+ */
+ @NotBlank(message = "{posts.posts_title} {err.required}")
+ private String postsTitle;
+
+ /**
+ * 게시물 내용
+ */
+ @NotBlank(message = "{posts.posts_content} {err.required}")
+ private String postsContent;
+
+ /**
+ * 첨부파일 코드
+ */
+ private String attachmentCode;
+
+ /**
+ * 게시물 등록 요청 DTO 클래스 생성자
+ * 빌더 패턴으로 객체 생성
+ *
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param attachmentCode 첨부파일 코드
+ */
+ @Builder
+ public PostsSimpleSaveRequestDto(String postsTitle, String postsContent, String attachmentCode) {
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.attachmentCode = attachmentCode;
+ }
+
+ /**
+ * 게시물 등록 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Posts 게시물 엔티티
+ */
+ public Posts toEntity(Integer boardNo, Integer postsNo) {
+ return Posts.builder()
+ .board(Board.builder()
+ .boardNo(boardNo)
+ .build())
+ .postsId(PostsId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .build())
+ .postsTitle(postsTitle)
+ .postsContent(postsContent)
+ .attachmentCode(attachmentCode)
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsUpdateRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsUpdateRequestDto.java
new file mode 100644
index 0000000..b9cfe53
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsUpdateRequestDto.java
@@ -0,0 +1,94 @@
+package org.egovframe.cloud.boardservice.api.posts.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.dto.PostsUpdateRequestDto
+ *
+ * 게시물 수정 요청 DTO 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/08
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/08 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class PostsUpdateRequestDto {
+
+ /**
+ * 게시물 제목
+ */
+ @NotBlank(message = "{posts.posts_title} {err.required}")
+ private String postsTitle;
+
+ /**
+ * 게시물 내용
+ */
+ @NotBlank(message = "{posts.posts_content} {err.required}")
+ private String postsContent;
+
+ /**
+ * 게시물 답변 내용
+ */
+ private String postsAnswerContent;
+
+ /**
+ * 첨부파일 코드
+ */
+ private String attachmentCode;
+
+ /**
+ * 공지 여부
+ */
+ @NotNull(message = "{posts.notice_at} {err.required}")
+ private Boolean noticeAt;
+
+ /**
+ * 게시물 등록 요청 DTO 클래스 생성자
+ * 빌더 패턴으로 객체 생성
+ *
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param postsAnswerContent 게시물 답변 내용
+ * @param attachmentCode 첨부파일 코드
+ * @param noticeAt 공지 여부
+ */
+ @Builder
+ public PostsUpdateRequestDto(String postsTitle, String postsContent, String postsAnswerContent, String attachmentCode, Boolean noticeAt) {
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.postsAnswerContent = postsAnswerContent;
+ this.attachmentCode = attachmentCode;
+ this.noticeAt = noticeAt;
+ }
+
+ /**
+ * 게시물 등록 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성
+ *
+ * @return Posts 게시물 엔티티
+ */
+ public Posts toEntity() {
+ return Posts.builder()
+ .postsTitle(postsTitle)
+ .postsContent(postsContent)
+ .postsAnswerContent(postsAnswerContent)
+ .attachmentCode(attachmentCode)
+ .noticeAt(noticeAt)
+ .build();
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SecurityConfig.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SecurityConfig.java
new file mode 100644
index 0000000..76cc942
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SecurityConfig.java
@@ -0,0 +1,65 @@
+package org.egovframe.cloud.boardservice.config;
+
+import lombok.RequiredArgsConstructor;
+import org.egovframe.cloud.servlet.config.AuthenticationFilter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+
+/**
+ * org.egovframe.cloud.boardservice.config.SecurityConfig
+ *
+ * Spring Security Config 클래스
+ * AuthenticationFilter 를 추가하고 토큰으로 setAuthentication 인증처리를 한다
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/06/30
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/06/30 jaeyeolkim 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+@EnableWebSecurity // Spring Security 설정들을 활성화시켜 준다
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+ @Value("${token.secret}")
+ private String TOKEN_SECRET;
+
+ /**
+ * 스프링 시큐리티 설정
+ *
+ * @param http
+ * @throws Exception
+ */
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .csrf().disable()
+ .headers().frameOptions().disable()
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 사용하기 때문에 세션은 비활성화
+ .and()
+ .addFilter(getAuthenticationFilter());
+ }
+
+ /**
+ * 토큰에 담긴 정보로 Authentication 정보를 설정하여 jpa audit 처리에 사용된다.
+ * 이 처리를 하지 않으면 AnonymousAuthenticationToken 으로 처리된다.
+ *
+ * @return
+ * @throws Exception
+ */
+ private AuthenticationFilter getAuthenticationFilter() throws Exception {
+ return new AuthenticationFilter(authenticationManager(), TOKEN_SECRET);
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SqlQueryConfig.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SqlQueryConfig.java
new file mode 100644
index 0000000..e396c38
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SqlQueryConfig.java
@@ -0,0 +1,56 @@
+package org.egovframe.cloud.boardservice.config;
+
+import com.querydsl.sql.MySQLTemplates;
+import com.querydsl.sql.SQLQueryFactory;
+import com.querydsl.sql.SQLTemplates;
+import com.querydsl.sql.spring.SpringConnectionProvider;
+import com.querydsl.sql.spring.SpringExceptionTranslator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.sql.DataSource;
+
+/**
+ * org.egovframe.cloud.common.config.SqlQueryConfig
+ *
+ * Native SQL 설정 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/07
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/07 jooho 최초 생성
+ *
+ */
+@Configuration
+public class SqlQueryConfig {
+
+ /**
+ * SQLQueryFactory 빈 등록
+ *
+ * @param dataSource 데이터 소스
+ * @return SQLQueryFactory 쿼리 및 DML 절 생성을 위한 팩토리 클래스
+ */
+ @Bean
+ public SQLQueryFactory queryFactory(DataSource dataSource) {
+ return new SQLQueryFactory(querydslConfiguration(), new SpringConnectionProvider(dataSource));
+ }
+
+ /**
+ * querydsl 설정
+ *
+ * @return Configuration 설정
+ */
+ public com.querydsl.sql.Configuration querydslConfiguration() {
+ SQLTemplates templates = MySQLTemplates.builder().build(); // MySQL
+ com.querydsl.sql.Configuration configuration = new com.querydsl.sql.Configuration(templates);
+ configuration.setExceptionTranslator(new SpringExceptionTranslator());
+ return configuration;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/Board.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/Board.java
new file mode 100644
index 0000000..5819021
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/Board.java
@@ -0,0 +1,193 @@
+package org.egovframe.cloud.boardservice.domain.board;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.servlet.domain.BaseEntity;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.board.Board
+ *
+ * 게시판 엔티티 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+@Entity
+public class Board extends BaseEntity {
+
+ /**
+ * 게시판 번호
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(columnDefinition = "int(9)")
+ private Integer boardNo;
+
+ /**
+ * 게시판 제목
+ */
+ @Column(nullable = false, length = 100)
+ private String boardName;
+
+ /**
+ * 스킨 유형 코드
+ */
+ @Column(nullable = false, length = 20)
+ private String skinTypeCode;
+
+ /**
+ * 제목 표시 길이
+ */
+ @Column(nullable = false, columnDefinition = "mediumint(5) default '20'")
+ private Integer titleDisplayLength;
+
+ /**
+ * 게시물 표시 수
+ */
+ @Column(nullable = false, columnDefinition = "mediumint(5) default '10'")
+ private Integer postDisplayCount;
+
+ /**
+ * 페이지 표시 수
+ */
+ @Column(nullable = false, columnDefinition = "mediumint(5) default '10'")
+ private Integer pageDisplayCount;
+
+ /**
+ * 신규 표시 일 수
+ */
+ @Column(nullable = false, columnDefinition = "mediumint(5) default '3'")
+ private Integer newDisplayDayCount;
+
+ /**
+ * 에디터 사용 여부
+ */
+ @Column(nullable = false, columnDefinition = "tinyint(1) default '0'")
+ private Boolean editorUseAt;
+
+ /**
+ * 사용자 작성 여부
+ */
+ @Column(nullable = false, columnDefinition = "tinyint(1) default '0'")
+ private Boolean userWriteAt;
+
+ /**
+ * 댓글 사용 여부
+ */
+ @Column(nullable = false, columnDefinition = "tinyint(1) default '0'")
+ private Boolean commentUseAt;
+
+ /**
+ * 업로드 사용 여부
+ */
+ @Column(nullable = false, columnDefinition = "tinyint(1) default '0'")
+ private Boolean uploadUseAt;
+
+ /**
+ * 업로드 제한 수
+ */
+ @Column(columnDefinition = "mediumint(5)")
+ private Integer uploadLimitCount;
+
+ /**
+ * 업로드 제한 크기
+ */
+ @Column(columnDefinition = "bigint(20)")
+ private BigDecimal uploadLimitSize;
+
+ /**
+ * 게시물 엔티티
+ */
+ /*@OneToMany(mappedBy = "board", fetch = FetchType.LAZY)
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ private List posts;*/
+
+ /**
+ * 빌더 패턴 클래스 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param boardName 게시판 명
+ * @param skinTypeCode 스킨 유형 코드
+ * @param titleDisplayLength 제목 표시 길이
+ * @param postDisplayCount 게시물 표시 수
+ * @param pageDisplayCount 페이지 표시 수
+ * @param newDisplayDayCount 신규 표시 일 수
+ * @param editorUseAt 에디터 사용 여부
+ * @param userWriteAt 사용자 작성 여부
+ * @param commentUseAt 댓글 사용 여부
+ * @param uploadUseAt 업로드 사용 여부
+ * @param uploadLimitCount 업로드 제한 수
+ * @param uploadLimitSize 업로드 제한 크기
+ */
+ @Builder
+ public Board(Integer boardNo, String boardName, String skinTypeCode, Integer titleDisplayLength,
+ Integer postDisplayCount, Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt,
+ Boolean userWriteAt, Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount,
+ BigDecimal uploadLimitSize) {
+ this.boardNo = boardNo;
+ this.boardName = boardName;
+ this.skinTypeCode = skinTypeCode;
+ this.titleDisplayLength = titleDisplayLength;
+ this.postDisplayCount = postDisplayCount;
+ this.pageDisplayCount = pageDisplayCount;
+ this.newDisplayDayCount = newDisplayDayCount;
+ this.editorUseAt = editorUseAt;
+ this.userWriteAt = userWriteAt;
+ this.commentUseAt = commentUseAt;
+ this.uploadUseAt = uploadUseAt;
+ this.uploadLimitCount = uploadLimitCount;
+ this.uploadLimitSize = uploadLimitSize;
+ }
+
+ /**
+ * 게시판 속성 값 수정
+ *
+ * @param boardName 게시판 명
+ * @param skinTypeCode 스킨 유형 코드
+ * @param titleDisplayLength 제목 표시 길이
+ * @param postDisplayCount 게시물 표시 수
+ * @param pageDisplayCount 페이지 표시 수
+ * @param newDisplayDayCount 신규 표시 일 수
+ * @param userWriteAt 사용자 작성 여부
+ * @param editorUseAt 에디터 사용 여부
+ * @param commentUseAt 댓글 사용 여부
+ * @param uploadUseAt 업로드 사용 여부
+ * @param uploadLimitCount 업로드 제한 수
+ * @param uploadLimitSize 업로드 제한 크기
+ * @return Board 게시판 엔티티
+ */
+ public Board update(String boardName, String skinTypeCode, Integer titleDisplayLength, Integer postDisplayCount,
+ Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, Boolean userWriteAt,
+ Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, BigDecimal uploadLimitSize) {
+ this.boardName = boardName;
+ this.skinTypeCode = skinTypeCode;
+ this.titleDisplayLength = titleDisplayLength;
+ this.postDisplayCount = postDisplayCount;
+ this.pageDisplayCount = pageDisplayCount;
+ this.newDisplayDayCount = newDisplayDayCount;
+ this.editorUseAt = editorUseAt;
+ this.userWriteAt = userWriteAt;
+ this.commentUseAt = commentUseAt;
+ this.uploadUseAt = uploadUseAt;
+ this.uploadLimitCount = uploadLimitCount;
+ this.uploadLimitSize = uploadLimitSize;
+
+ return this;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepository.java
new file mode 100644
index 0000000..c7e76e6
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepository.java
@@ -0,0 +1,24 @@
+package org.egovframe.cloud.boardservice.domain.board;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.board.BoardRepository
+ *
+ * 게시판 레파지토리 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+public interface BoardRepository extends JpaRepository, BoardRepositoryCustom {
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryCustom.java
new file mode 100644
index 0000000..db24b0b
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryCustom.java
@@ -0,0 +1,47 @@
+package org.egovframe.cloud.boardservice.domain.board;
+
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.board.BoardRepositoryCustom
+ *
+ * 게시판 Querydsl 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+public interface BoardRepositoryCustom {
+
+ /**
+ * 게시판 페이지 목록 조회
+ *
+ * @param requestDto 게시판 목록 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시판 목록 응답 DTO
+ */
+ Page findPage(RequestDto requestDto, Pageable pageable);
+
+ /**
+ * 게시판 목록 조회
+ *
+ * @param boardNos 게시판 번호 목록
+ * @return List 게시판 상세 응답 DTO List
+ */
+ List findAllByBoardNoIn(List boardNos);
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryImpl.java
new file mode 100644
index 0000000..9905843
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryImpl.java
@@ -0,0 +1,143 @@
+package org.egovframe.cloud.boardservice.domain.board;
+
+import java.util.List;
+
+import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.QBoardListResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.QBoardResponseDto;
+import org.egovframe.cloud.boardservice.domain.code.QCode;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+
+import com.google.common.base.CaseFormat;
+import com.querydsl.core.QueryResults;
+import com.querydsl.core.types.Order;
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.core.types.dsl.Expressions;
+import com.querydsl.jpa.JPQLQuery;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.board.BoardRepositoryImpl
+ *
+ * 게시판 Querydsl 구현 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+public class BoardRepositoryImpl implements BoardRepositoryCustom {
+
+ /**
+ * DML 생성을위한 Querydsl 팩토리 클래스
+ */
+ private final JPAQueryFactory jpaQueryFactory;
+
+ /**
+ * 게시판 페이지 목록 조회
+ * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생
+ *
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시판 목록 응답 DTO
+ */
+ @Override
+ public Page findPage(RequestDto requestDto, Pageable pageable) {
+ JPQLQuery query = jpaQueryFactory
+ .select(new QBoardListResponseDto(
+ QBoard.board.boardNo,
+ QBoard.board.boardName,
+ QBoard.board.skinTypeCode,
+ Expressions.as(QCode.code.codeName, "skinTypeCodeName"),
+ QBoard.board.createdDate
+ ))
+ .from(QBoard.board)
+ .leftJoin(QCode.code).on(QBoard.board.skinTypeCode.eq(QCode.code.codeId).and(QCode.code.parentCodeId.eq("skin_type_code")))
+ .fetchJoin()
+ .where(getBooleanExpressionKeyword(requestDto));
+
+ //정렬
+ pageable.getSort().stream().forEach(sort -> {
+ Order order = sort.isAscending() ? Order.ASC : Order.DESC;
+ String property = sort.getProperty();
+
+ Path target = Expressions.path(Object.class, QBoard.board, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property));
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ OrderSpecifier> orderSpecifier = new OrderSpecifier(order, target);
+ query.orderBy(orderSpecifier);
+ });
+
+ QueryResults result = query
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize()) //페이징
+ .fetchResults();
+
+ return new PageImpl<>(result.getResults(), pageable, result.getTotal());
+ }
+
+ /**
+ * 게시판 목록 조회
+ *
+ * @param boardNos 게시판 번호 목록
+ * @return List 게시판 상세 응답 DTO List
+ */
+ @Override
+ public List findAllByBoardNoIn(List boardNos) {
+ return jpaQueryFactory
+ .select(new QBoardResponseDto(
+ QBoard.board.boardNo,
+ QBoard.board.boardName,
+ QBoard.board.skinTypeCode,
+ QBoard.board.titleDisplayLength,
+ QBoard.board.postDisplayCount,
+ QBoard.board.pageDisplayCount,
+ QBoard.board.newDisplayDayCount,
+ QBoard.board.editorUseAt,
+ QBoard.board.userWriteAt,
+ QBoard.board.commentUseAt,
+ QBoard.board.uploadUseAt,
+ QBoard.board.uploadLimitCount,
+ QBoard.board.uploadLimitSize
+ ))
+ .from(QBoard.board)
+ .leftJoin(QCode.code).on(QBoard.board.skinTypeCode.eq(QCode.code.codeId).and(QCode.code.parentCodeId.eq("skin_type_code")))
+ .fetchJoin()
+ .where(QBoard.board.boardNo.in(boardNos))
+ .orderBy(QBoard.board.boardNo.asc())
+ .fetchResults().getResults();
+ }
+
+ /**
+ * 요청 DTO로 동적 검색 표현식 리턴
+ *
+ * @param requestDto 요청 DTO
+ * @return BooleanExpression 검색 표현식
+ */
+ private BooleanExpression getBooleanExpressionKeyword(RequestDto requestDto) {
+ if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null;
+
+ switch (requestDto.getKeywordType()) {
+ case "boardName": // 게시판 명
+ return QBoard.board.boardName.containsIgnoreCase(requestDto.getKeyword());
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/code/Code.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/code/Code.java
new file mode 100644
index 0000000..36dc1a1
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/code/Code.java
@@ -0,0 +1,41 @@
+package org.egovframe.cloud.boardservice.domain.code;
+
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.servlet.domain.BaseEntity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.code.Code
+ *
+ * 공통코드 엔티티
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/07/12
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/12 jaeyeolkim 최초 생성
+ *
+ */
+@NoArgsConstructor
+@Entity
+public class Code extends BaseEntity {
+
+ @Id
+ @Column(insertable = false, updatable = false)
+ private String codeId; // 코드ID
+
+ @Column(insertable = false, updatable = false)
+ private String parentCodeId; // 상위 코드ID
+
+ @Column(insertable = false, updatable = false)
+ private String codeName; // 코드 명
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/Comment.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/Comment.java
new file mode 100644
index 0000000..f6f69c4
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/Comment.java
@@ -0,0 +1,149 @@
+package org.egovframe.cloud.boardservice.domain.comment;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.user.User;
+import org.egovframe.cloud.servlet.domain.BaseEntity;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
+
+import javax.persistence.*;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.comment.Comment
+ *
+ * 댓글 엔티티 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+@DynamicInsert
+@DynamicUpdate
+@Entity
+public class Comment extends BaseEntity {
+
+ /**
+ * 댓글 복합키
+ */
+ @EmbeddedId
+ private CommentId commentId;
+
+ /**
+ * 댓글 내용
+ */
+ @Column(nullable = false, length = 2000)
+ private String commentContent;
+
+ /**
+ * 그룹 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer groupNo;
+
+ /**
+ * 부모 댓글 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer parentCommentNo;
+
+ /**
+ * 깊이 순서
+ */
+ @Column(nullable = false, columnDefinition = "smallint(3)")
+ private Integer depthSeq;
+
+ /**
+ * 정렬 순서
+ */
+ @Column(nullable = false, columnDefinition = "int(9)")
+ private Integer sortSeq;
+
+ /**
+ * 삭제 여부
+ */
+ @Column(nullable = false, columnDefinition = "tinyint(1) default '0'")
+ private Integer deleteAt; // 0:미삭제, 1:작성자삭제, 2:관리자삭제
+
+ /**
+ * 생성자 엔티티
+ */
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "createdBy", referencedColumnName = "userId", insertable = false, updatable = false)
+ private User creator;
+
+ /**
+ * 게시물 엔티티
+ */
+ @MapsId("postsId") // CommentId.postsId 매핑
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumns({
+ @JoinColumn(name = "board_no"),
+ @JoinColumn(name = "posts_no")
+ })
+ private Posts posts;
+
+ /**
+ * 빌더 패턴 클래스 생성자
+ *
+ * @param posts 게시물 엔티티
+ * @param commentId 댓글 복합키
+ * @param commentContent 게시물 내용
+ * @param groupNo 그룹 번호
+ * @param parentCommentNo 부모 댓글 번호
+ * @param depthSeq 깊이 순서
+ * @param sortSeq 정렬 순서
+ * @param deleteAt 삭제 여부
+ */
+ @Builder
+ public Comment(Posts posts, CommentId commentId, String commentContent,
+ Integer groupNo, Integer parentCommentNo, Integer depthSeq,
+ Integer sortSeq, Integer deleteAt, User creator) {
+ this.posts = posts;
+ this.commentId = commentId;
+ this.commentContent = commentContent;
+ this.groupNo = groupNo;
+ this.parentCommentNo = parentCommentNo;
+ this.depthSeq = depthSeq;
+ this.sortSeq = sortSeq;
+ this.deleteAt = deleteAt;
+ this.creator = creator;
+ }
+
+ /**
+ * 댓글 내용 수정
+ *
+ * @param commentContent 게시물 내용
+ * @return Comment 댓글 엔티티
+ */
+ public Comment update(String commentContent) {
+ this.commentContent = commentContent;
+
+ return this;
+ }
+
+ /**
+ * 댓글 삭제 여부 수정
+ *
+ * @param deleteAt 삭제 여부
+ * @return Comment 댓글 엔티티
+ */
+ public Comment updateDeleteAt(Integer deleteAt) {
+ this.deleteAt = deleteAt;
+
+ return this;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentId.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentId.java
new file mode 100644
index 0000000..539dbe0
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentId.java
@@ -0,0 +1,89 @@
+package org.egovframe.cloud.boardservice.domain.comment;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.comment.CommentId
+ *
+ * 댓글 엔티티 복합키 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+@Embeddable
+public class CommentId implements Serializable {
+
+ /**
+ * serialVersionUID
+ */
+ private static final long serialVersionUID = -8263227281325691911L;
+
+ /**
+ * 게시물 복합키
+ */
+ private PostsId postsId; // @MapsId("postsId")로 매핑
+
+ /**
+ * 댓글 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer commentNo;
+
+ /**
+ * 빌드 패턴 클래스 생성자
+ *
+ * @param postsId 게시물 복합키
+ * @param commentNo 댓글 번호
+ */
+ @Builder
+ public CommentId(PostsId postsId, Integer commentNo) {
+ this.postsId = postsId;
+ this.commentNo = commentNo;
+ }
+
+ /**
+ * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.
+ *
+ * @return int a hash code value for this object.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(postsId.getBoardNo(), postsId.getPostsNo(), commentNo);
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ *
+ * @param object the reference object with which to compare.
+ * @return {@code true} if this object is the same as the obj
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) return true;
+ if (!(object instanceof CommentId)) return false;
+ CommentId that = (CommentId) object;
+ return Objects.equals(postsId.getBoardNo(), that.getPostsId().getBoardNo()) &&
+ Objects.equals(postsId.getPostsNo(), that.getPostsId().getPostsNo()) &&
+ Objects.equals(commentNo, that.getCommentNo());
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepository.java
new file mode 100644
index 0000000..cc53f10
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepository.java
@@ -0,0 +1,24 @@
+package org.egovframe.cloud.boardservice.domain.comment;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.comment.CommentRepository
+ *
+ * 댓글 레파지토리 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+public interface CommentRepository extends JpaRepository, CommentRepositoryCustom {
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryCustom.java
new file mode 100644
index 0000000..9e0dac2
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryCustom.java
@@ -0,0 +1,112 @@
+package org.egovframe.cloud.boardservice.domain.comment;
+
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.comment.CommentRepositoryCustom
+ *
+ * 댓글 Querydsl 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+public interface CommentRepositoryCustom {
+
+ /**
+ * 댓글 전체 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param deleteAt 삭제 여부
+ * @return List 댓글 목록 응답 DTO
+ */
+ List findAll(Integer boardNo, Integer postsNo, Integer deleteAt);
+
+ /**
+ * 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param deleteAt 삭제 여부
+ * @param pageable 페이지 정보
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ Map findPage(Integer boardNo, Integer postsNo, Integer deleteAt, Pageable pageable);
+
+ /**
+ * 다음 댓글 번호 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Integer 다음 댓글 번호
+ */
+ Integer findNextCommentNo(Integer boardNo, Integer postsNo);
+
+ /**
+ * 다음 정렬 순서 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Integer 다음 댓글 번호
+ */
+ Integer findNextSortSeq(Integer boardNo, Integer postsNo);
+
+ /**
+ * 대댓글의 정렬 마지막 순서 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param groupNo 그룹 번호
+ * @return Integer 다음 댓글 번호
+ */
+ Integer findLastSortSeq(Integer boardNo, Integer postsNo, Integer groupNo);
+
+ /**
+ * 대댓글의 정렬 순서 조회
+ * 댓글그룹내에 부모 댓글 보다 정렬 순서가 크고 깊이가 크거나 같은 가장 작은 순서
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param groupNo 그룹 번호
+ * @param parentCommentNo 부모 게시물 번호
+ * @param depthSeq 깊이 순서
+ * @return Integer 다음 댓글 번호
+ */
+ Integer findNextSortSeq(Integer boardNo, Integer postsNo, Integer groupNo, Integer parentCommentNo, Integer depthSeq);
+
+ /**
+ * 댓글 삭제 여부 수정
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ * @param deleteAt 삭제 여부
+ * @return Integer 처리 건수
+ */
+ Long updateDeleteAt(Integer boardNo, Integer postsNo, Integer commentNo, Integer deleteAt);
+
+ /**
+ * 댓글 정렬 순서 수정
+ *
+ * @param groupNo 그룹 번호
+ * @param startSortSeq 시작 정렬 순서
+ * @param endSortSeq 종료 정렬 순서
+ * @param increaseSortSeq 증가 정렬 순서
+ * @return Long 처리 건수
+ */
+ Long updateSortSeq(Integer groupNo, Integer startSortSeq, Integer endSortSeq, int increaseSortSeq);
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryImpl.java
new file mode 100644
index 0000000..0dac4d8
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryImpl.java
@@ -0,0 +1,362 @@
+package org.egovframe.cloud.boardservice.domain.comment;
+
+import com.querydsl.core.Tuple;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.SubQueryExpression;
+import com.querydsl.core.types.dsl.*;
+import com.querydsl.jpa.JPAExpressions;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.querydsl.sql.SQLQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto;
+import org.egovframe.cloud.boardservice.api.comment.dto.QCommentListResponseDto;
+import org.egovframe.cloud.boardservice.domain.user.QUser;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.egovframe.cloud.boardservice.domain.comment.QComment.comment;
+import static org.egovframe.cloud.boardservice.domain.user.QUser.user;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.comment.CommentRepositoryImpl
+ *
+ * 댓글 Querydsl 구현 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+public class CommentRepositoryImpl implements CommentRepositoryCustom {
+
+ /**
+ * DML 생성을위한 Querydsl 팩토리 클래스
+ */
+ private final JPAQueryFactory jpaQueryFactory;
+
+ /**
+ * 쿼리 및 DML 절 생성을 위한 팩토리 클래스
+ */
+ private final SQLQueryFactory sqlQueryFactory;
+
+
+ /**
+ * 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param deleteAt 삭제 여부
+ * @return List 댓글 목록 응답 DTO
+ */
+ public List findAll(Integer boardNo, Integer postsNo, Integer deleteAt) {
+ return jpaQueryFactory
+ .select(new QCommentListResponseDto(
+ comment.commentId.postsId.boardNo,
+ comment.commentId.postsId.postsNo,
+ comment.commentId.commentNo,
+ comment.commentContent,
+ comment.groupNo,
+ comment.parentCommentNo,
+ comment.depthSeq,
+ comment.sortSeq,
+ comment.deleteAt,
+ comment.createdBy,
+ QUser.user.userName.as("createdName"),
+ comment.createdDate))
+ .from(comment)
+ .leftJoin(user).on(comment.createdBy.eq(user.userId))
+ .fetchJoin()
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo))
+ .and(isEqualsDeleteAt(deleteAt)))
+ .orderBy(comment.commentId.postsId.boardNo.asc(), comment.commentId.postsId.postsNo.asc(), comment.groupNo.asc(), comment.sortSeq.asc())
+ .fetch();
+ }
+
+ /**
+ * 댓글 목록 조회
+ *
+ * JPQL 은 from 절에서 서브쿼리를 사용할 수 없어서 SQLQueryFactory 를 사용해 Native SQL 로 조회
+ *
+ * Native SQL 을 사용하지 않고 서브쿼리를 먼저 조회한 후 JPQL 만을 사용해서 조회 가능
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param deleteAt 삭제 여부
+ * @param pageable 페이지 정보
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ public Map findPage(Integer boardNo, Integer postsNo, Integer deleteAt, Pageable pageable) {
+ // 전체 댓글 수, 최상위 댓글 수(페이징 기준) 조회
+ String totalElementsKey = "totalElements";
+ Tuple countInfo = jpaQueryFactory
+ .select(Expressions.asNumber(1).count().as(totalElementsKey),
+ new CaseBuilder()
+ .when(comment.parentCommentNo.isNull())
+ .then(1)
+ .otherwise(0)
+ .sum()
+ .coalesce(0)
+ .as("count"))
+ .from(comment)
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo))
+ .and(isEqualsDeleteAt(deleteAt)))
+ .fetchOne();
+
+ Long totalElements = 0L;
+ Integer groupElements = 0;
+ if (countInfo != null) {
+ totalElements = countInfo.get(Expressions.numberPath(Long.class, totalElementsKey)); // 전체 댓글 수
+ groupElements = countInfo.get(Expressions.numberPath(Integer.class, "count")); // 최상위 댓글 수
+ }
+
+ // path 정의
+ Path commentPath = Expressions.path(Comment.class, "comment");
+ NumberPath boardNoPath = Expressions.numberPath(Integer.class, commentPath, "board_no");
+ NumberPath postsNoPath = Expressions.numberPath(Integer.class, commentPath, "posts_no");
+ NumberPath commentNoPath = Expressions.numberPath(Integer.class, commentPath, "comment_no");
+ NumberPath groupNoPath = Expressions.numberPath(Integer.class, commentPath, "group_no");
+ NumberPath parentCommentNoPath = Expressions.numberPath(Integer.class, commentPath, "parent_comment_no");
+ NumberPath sortSeqPath = Expressions.numberPath(Integer.class, commentPath, "sort_seq");
+ NumberPath deleteAtPath = Expressions.numberPath(Integer.class, commentPath, "delete_at");
+
+ StringPath userPath = Expressions.stringPath("user");
+
+ BooleanExpression deleteAtExpression = null;
+ if (deleteAt != null) {
+ deleteAtExpression = deleteAt == 0 ? deleteAtPath.eq(0) : deleteAtPath.ne(0);
+ }
+
+ // 댓글 그룹 조회
+ Path groupCommentPath = Expressions.path(Comment.class, "groupComment");
+ SubQueryExpression groupComment = JPAExpressions.select(boardNoPath,
+ postsNoPath,
+ commentNoPath)
+ .from(comment)
+ .where(boardNoPath.eq(boardNo)
+ .and(postsNoPath.eq(postsNo))
+ .and(parentCommentNoPath.isNull())
+ .and(deleteAtExpression))
+ .orderBy(boardNoPath.asc(), postsNoPath.asc(), commentNoPath.asc())
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize());
+
+ // 댓글 조회
+ List comments = sqlQueryFactory
+ .select(new QCommentListResponseDto(
+ boardNoPath,
+ postsNoPath,
+ commentNoPath,
+ Expressions.stringPath(commentPath, "comment_content"),
+ groupNoPath,
+ parentCommentNoPath,
+ Expressions.numberPath(Integer.class, commentPath, "depth_seq"),
+ Expressions.numberPath(Integer.class, commentPath, "sort_seq"),
+ Expressions.numberPath(Integer.class, commentPath, "delete_at"),
+ Expressions.stringPath(commentPath, "created_by"),
+ Expressions.stringPath(userPath, "user_name"),
+ Expressions.datePath(LocalDateTime.class, commentPath, "created_date")))
+ .from(comment)
+ .innerJoin(groupComment, groupCommentPath).on(Expressions.numberPath(Integer.class, groupCommentPath, "board_no").eq(boardNoPath)
+ .and(Expressions.numberPath(Integer.class, groupCommentPath, "posts_no").eq(postsNoPath))
+ .and(Expressions.numberPath(Integer.class, groupCommentPath, "comment_no").eq(groupNoPath)))
+ .leftJoin(user).on(Expressions.stringPath(commentPath, "created_by").eq(Expressions.stringPath(userPath, "user_id")))
+ .where(boardNoPath.eq(boardNo)
+ .and(postsNoPath.eq(postsNo))
+ .and(deleteAtExpression))
+ .orderBy(boardNoPath.asc(), postsNoPath.asc(), groupNoPath.asc(), sortSeqPath.asc())
+ .fetch();
+
+ Page page = new PageImpl<>(comments, pageable, groupElements == null ? 0 : groupElements);
+
+ // 페이지 인터페이스와 동일한 속성의 맵 리턴
+ Map result = new HashMap<>();
+
+ result.put("content", comments);
+ result.put("empty", page.isEmpty());
+ result.put("first", page.isFirst());
+ result.put("last", page.isLast());
+ result.put("number", page.getNumber());
+ result.put("numberOfElements", comments.size());
+ result.put("size", page.getSize());
+ result.put(totalElementsKey, totalElements);
+ result.put("groupElements", groupElements);
+ result.put("totalPages", page.getTotalPages());
+
+ return result;
+ }
+
+ /**
+ * 다음 댓글 번호 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Integer 다음 게시물 번호
+ */
+ public Integer findNextCommentNo(Integer boardNo, Integer postsNo) {
+ return jpaQueryFactory
+ .select(comment.commentId.commentNo.max().add(1).coalesce(1))
+ .from(comment)
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo)))
+ .fetchOne();
+ }
+
+ /**
+ * 다음 정렬 순서 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Integer 다음 게시물 번호
+ */
+ public Integer findNextSortSeq(Integer boardNo, Integer postsNo) {
+ return jpaQueryFactory
+ .select(comment.sortSeq.max().add(1).coalesce(1))
+ .from(comment)
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo))
+ .and(comment.parentCommentNo.isNull()))
+ .fetchOne();
+ }
+
+ /**
+ * 대댓글의 정렬 마지막 순서 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param groupNo 그룹 번호
+ * @return Integer 다음 게시물 번호
+ */
+ public Integer findLastSortSeq(Integer boardNo, Integer postsNo, Integer groupNo) {
+ return jpaQueryFactory
+ .select(comment.sortSeq.max().add(1).coalesce(1))
+ .from(comment)
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo))
+ .and(comment.groupNo.eq(groupNo)))
+ .fetchOne();
+ }
+
+ /**
+ * 대댓글의 정렬 순서 조회
+ * 댓글그룹내에 부모 댓글 보다 정렬 순서가 크고 깊이가 크거나 같은 가장 작은 순서
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param groupNo 그룹 번호
+ * @param parentCommentNo 부모 게시물 번호
+ * @param depthSeq 깊이 순서
+ * @return Integer 다음 게시물 번호
+ */
+ public Integer findNextSortSeq(Integer boardNo, Integer postsNo, Integer groupNo, Integer parentCommentNo, Integer depthSeq) {
+ return jpaQueryFactory
+ .select(comment.sortSeq.min())
+ .from(comment)
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo))
+ .and(comment.groupNo.eq(groupNo))
+ .and(comment.sortSeq.gt(JPAExpressions.select(comment.sortSeq)
+ .from(comment)
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo))
+ .and(comment.commentId.commentNo.eq(parentCommentNo)))))
+ .and(comment.depthSeq.lt(depthSeq)))
+ .fetchOne();
+ }
+
+ /**
+ * 댓글 정렬 순서 수정
+ *
+ * @param groupNo 그룹 번호
+ * @param startSortSeq 시작 정렬 순서
+ * @param endSortSeq 종료 정렬 순서
+ * @param increaseSortSeq 증가 정렬 순서
+ * @return Long 수정 건수
+ */
+ public Long updateSortSeq(Integer groupNo, Integer startSortSeq, Integer endSortSeq, int increaseSortSeq) {
+ return jpaQueryFactory.update(comment)
+ .set(comment.sortSeq, comment.sortSeq.add(increaseSortSeq))
+ .where(isEqualsGroupNo(groupNo),
+ isGoeSortSeq(startSortSeq),
+ isLoeSortSeq(endSortSeq))
+ .execute();
+ }
+
+ /**
+ * 댓글 삭제 여부 수정
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ * @param deleteAt 삭제 여부
+ * @return Integer 처리 건수
+ */
+ public Long updateDeleteAt(Integer boardNo, Integer postsNo, Integer commentNo, Integer deleteAt) {
+ return jpaQueryFactory.update(comment)
+ .set(comment.deleteAt, deleteAt)
+ .set(comment.modifiedDate, LocalDateTime.now())
+ .where(comment.commentId.postsId.boardNo.eq(boardNo)
+ .and(comment.commentId.postsId.postsNo.eq(postsNo))
+ .and(comment.commentId.commentNo.eq(commentNo)))
+ .execute();
+ }
+
+ /**
+ * 삭제여부 검색 표현식
+ *
+ * @param deleteAt 삭제 여부
+ * @return BooleanExpression 검색 표현식
+ */
+ private BooleanExpression isEqualsDeleteAt(Integer deleteAt) {
+ if (deleteAt == null) return null;
+
+ if (deleteAt == 0) return comment.deleteAt.eq(deleteAt);
+ else return comment.deleteAt.ne(0);
+ }
+
+ /**
+ * 그룹 번호 동일 검색 표현식
+ *
+ * @param groupNo 그룹 번호
+ * @return BooleanExpression 검색 표현식
+ */
+ private BooleanExpression isEqualsGroupNo(Integer groupNo) {
+ return groupNo == null ? null : comment.groupNo.eq(groupNo);
+ }
+
+ /**
+ * 정렬 순서 이하 검색 표현식
+ *
+ * @param sortSeq 정렬 순서
+ * @return BooleanExpression 검색 표현식
+ */
+ private BooleanExpression isLoeSortSeq(Integer sortSeq) {
+ return sortSeq == null ? null : comment.sortSeq.loe(sortSeq);
+ }
+
+ /**
+ * 정렬 순서 이상 검색 표현식
+ *
+ * @param sortSeq 정렬 순서
+ * @return BooleanExpression 검색 표현식
+ */
+ private BooleanExpression isGoeSortSeq(Integer sortSeq) {
+ return sortSeq == null ? null : comment.sortSeq.goe(sortSeq);
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/Posts.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/Posts.java
new file mode 100644
index 0000000..a7627ae
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/Posts.java
@@ -0,0 +1,202 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.comment.Comment;
+import org.egovframe.cloud.boardservice.domain.user.User;
+import org.egovframe.cloud.servlet.domain.BaseEntity;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+
+import javax.persistence.*;
+import java.util.List;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.Posts
+ *
+ * 게시물 엔티티 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+@DynamicInsert
+@DynamicUpdate
+@Entity
+public class Posts extends BaseEntity {
+
+ /**
+ * 게시물 복합키
+ */
+ @EmbeddedId
+ private PostsId postsId;
+
+ /**
+ * 게시판 엔티티
+ */
+ @MapsId("boardNo") // PostsId.boardNo 매핑
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "board_no")
+ private Board board;
+
+ /**
+ * 게시물 제목
+ */
+ @Column(nullable = false, length = 100)
+ private String postsTitle;
+
+ /**
+ * 게시물 내용
+ */
+ @Column(nullable = false, columnDefinition = "longtext")
+ private String postsContent;
+
+ /**
+ * 게시물 내용
+ */
+ @Column(columnDefinition = "longtext")
+ private String postsAnswerContent;
+
+ /**
+ * 첨부파일 코드
+ */
+ @Column
+ private String attachmentCode;
+
+ /**
+ * 조회 수
+ */
+ @Column(columnDefinition = "int(9) default '0'")
+ private Integer readCount;
+
+ /**
+ * 공지 여부
+ */
+ @Column(nullable = false, columnDefinition = "tinyint(1) default '0'")
+ private Boolean noticeAt;
+
+ /**
+ * 삭제 여부
+ */
+ @Column(nullable = false, columnDefinition = "tinyint(1) default '0'")
+ private Integer deleteAt;
+
+ /**
+ * 생성자 엔티티
+ */
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "createdBy", referencedColumnName = "userId", insertable = false, updatable = false)
+ private User creator;
+
+ /**
+ * 댓글 엔티티
+ */
+ @OneToMany(mappedBy = "posts", fetch = FetchType.LAZY)
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ private List comments;
+
+ /**
+ * 빌더 패턴 클래스 생성자
+ *
+ * @param board 게시판 엔티티
+ * @param postsId 게시물 복합키
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param postsAnswerContent 게시물 답변 내용
+ * @param attachmentCode 첨부파일 코드
+ * @param readCount 조회 수
+ * @param noticeAt 공지 여부
+ * @param deleteAt 삭제 여부
+ */
+ @Builder
+ public Posts(Board board, PostsId postsId, String postsTitle,
+ String postsContent, String postsAnswerContent, String attachmentCode,
+ Integer readCount, Boolean noticeAt, Integer deleteAt,
+ User creator, List comments) {
+ this.board = board;
+ this.postsId = postsId;
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.postsAnswerContent = postsAnswerContent;
+ this.attachmentCode = attachmentCode;
+ this.readCount = readCount;
+ this.noticeAt = noticeAt;
+ this.deleteAt = deleteAt;
+ this.creator = creator;
+ this.comments = comments;
+ }
+
+ /**
+ * 게시물 속성 값 수정
+ *
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param attachmentCode 첨부파일 코드
+ * @return Posts 게시물 엔티티
+ */
+ public Posts update(String postsTitle, String postsContent, String attachmentCode) {
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.attachmentCode = attachmentCode;
+
+ return this;
+ }
+
+ /**
+ * 게시물 속성 값 수정
+ *
+ * @param postsTitle 게시물 제목
+ * @param postsContent 게시물 내용
+ * @param postsAnswerContent 게시물 답변 내용
+ * @param attachmentCode 첨부파일 코드
+ * @param noticeAt 공지 여부
+ * @return Posts 게시물 엔티티
+ */
+ public Posts update(String postsTitle, String postsContent, String postsAnswerContent, String attachmentCode, Boolean noticeAt) {
+ this.postsTitle = postsTitle;
+ this.postsContent = postsContent;
+ this.postsAnswerContent = postsAnswerContent;
+ this.attachmentCode = attachmentCode;
+ this.noticeAt = noticeAt;
+
+ return this;
+ }
+
+ /**
+ * 게시물 삭제 여부 수정
+ *
+ * @param deleteAt 삭제 여부
+ * @return Posts 게시물 엔티티
+ */
+ public Posts updateDeleteAt(Integer deleteAt) {
+ this.deleteAt = deleteAt;
+
+ return this;
+ }
+
+ /**
+ * 조회 수 증가
+ *
+ * @return Posts 게시물 엔티티
+ */
+ public Posts updateReadCount() {
+ this.readCount += 1;
+
+ return this;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsId.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsId.java
new file mode 100644
index 0000000..13cc5f1
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsId.java
@@ -0,0 +1,92 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsId
+ *
+ * 게시물 엔티티 복합키 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/30
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/30 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+@Embeddable
+public class PostsId implements Serializable {
+
+ /**
+ * serialVersionUID
+ */
+ private static final long serialVersionUID = 2286680637185590124L;
+
+ /**
+ * 게시판 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer boardNo; // @MapsId("boardNo")로 매핑
+
+ /**
+ * 게시물 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer postsNo;
+
+ /**
+ * 빌드 패턴 클래스 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ */
+ @Builder
+ public PostsId(Integer boardNo, Integer postsNo) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ }
+
+ /**
+ * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.
+ *
+ * @return int a hash code value for this object.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(boardNo, postsNo);
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ *
+ * @param object the reference object with which to compare.
+ * @return {@code true} if this object is the same as the obj
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) return true;
+ if (!(object instanceof PostsId)) return false;
+ PostsId that = (PostsId) object;
+ return Objects.equals(boardNo, that.getBoardNo()) &&
+ Objects.equals(postsNo, that.getPostsNo());
+ }
+
+ @Override
+ public String toString() {
+ return this.boardNo+"_"+this.postsNo;
+ }
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRead.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRead.java
new file mode 100644
index 0000000..d8c38f9
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRead.java
@@ -0,0 +1,83 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.Column;
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.EntityListeners;
+import java.time.LocalDateTime;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsRead
+ *
+ * 게시물 조회 엔티티 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/02
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/02 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+@Entity
+@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함
+public class PostsRead {
+
+ /**
+ * 게시물 조회 복합키
+ */
+ @EmbeddedId
+ private PostsReadId postsReadId;
+
+ /**
+ * 사용자 id
+ */
+ private String userId;
+
+ /**
+ * ip 주소
+ */
+ @Column(nullable = false, columnDefinition = "varchar(50)")
+ private String ipAddr;
+
+ /**
+ * 생성 일시
+ */
+ @CreatedDate
+ @Column
+ private LocalDateTime createdDate;
+
+ /**
+ * 빌더 패턴 클래스 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param readNo 조회 번호
+ * @param userId 사용자 id
+ * @param tokenId 토큰 id
+ * @param ipAddr ip 주소
+ */
+ @Builder
+ public PostsRead(Integer boardNo, Integer postsNo, Integer readNo, String userId, String tokenId, String ipAddr) {
+ this.postsReadId = PostsReadId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .readNo(readNo)
+ .build();
+ this.userId = userId;
+ this.ipAddr = ipAddr;
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadId.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadId.java
new file mode 100644
index 0000000..c25f093
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadId.java
@@ -0,0 +1,97 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsReadId
+ *
+ * 게시판 조회 엔티티 복합키 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/02
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/02 jooho 최초 생성
+ *
+ */
+@Getter
+@NoArgsConstructor
+@Embeddable
+public class PostsReadId implements Serializable {
+
+ /**
+ * serialVersionUID
+ */
+ private static final long serialVersionUID = -6710005976442877773L;
+
+ /**
+ * 게시판 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer boardNo;
+
+ /**
+ * 게시물 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer postsNo;
+
+ /**
+ * 조회 번호
+ */
+ @Column(columnDefinition = "int(9)")
+ private Integer readNo;
+
+ /**
+ * 빌드 패턴 클래스 생성자
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param readNo 조회 번호
+ */
+ @Builder
+ public PostsReadId(Integer boardNo, Integer postsNo, Integer readNo) {
+ this.boardNo = boardNo;
+ this.postsNo = postsNo;
+ this.readNo = readNo;
+ }
+
+ /**
+ * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.
+ *
+ * @return int a hash code value for this object.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(boardNo, postsNo, readNo);
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ *
+ * @param object the reference object with which to compare.
+ * @return {@code true} if this object is the same as the obj
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) return true;
+ if (!(object instanceof PostsReadId)) return false;
+ PostsReadId that = (PostsReadId) object;
+ return Objects.equals(boardNo, that.getBoardNo()) &&
+ Objects.equals(postsNo, that.getPostsNo()) &&
+ Objects.equals(readNo, that.getReadNo());
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepository.java
new file mode 100644
index 0000000..de5a432
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepository.java
@@ -0,0 +1,24 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsReadRepository
+ *
+ * 게시물 조회 레파지토리 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/02
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/02 jooho 최초 생성
+ *
+ */
+public interface PostsReadRepository extends JpaRepository, PostsReadRepositoryCustom {
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryCustom.java
new file mode 100644
index 0000000..0e7cfd3
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryCustom.java
@@ -0,0 +1,42 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsReadRepositoryCustom
+ *
+ * 게시물 조회 Querydsl 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/02
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/02 jooho 최초 생성
+ *
+ */
+public interface PostsReadRepositoryCustom {
+
+ /**
+ * 게시물 조회 데이터 수 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param userId 사용자 id
+ * @param ipAddr ip 주소
+ * @return Long 데이터 수
+ */
+ Long countByBoardNoAndPostsNoAndUserId(Integer boardNo, Integer postsNo, String userId, String ipAddr);
+
+ /**
+ * 다음 게시물 조회 번호 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Integer 다음 게시물 조회 번호
+ */
+ Integer findNextReadNo(Integer boardNo, Integer postsNo);
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryImpl.java
new file mode 100644
index 0000000..a589efd
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryImpl.java
@@ -0,0 +1,86 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsReadRepositoryImpl
+ *
+ * 게시물 조회 Querydsl 구현 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/02
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/02 jooho 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+public class PostsReadRepositoryImpl implements PostsReadRepositoryCustom {
+
+ /**
+ * DML 생성을위한 Querydsl 팩토리 클래스
+ */
+ private final JPAQueryFactory jpaQueryFactory;
+
+ /**
+ * 게시물 조회 데이터 수 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param userId 사용자 id
+ * @param ipAddr ip 주소
+ * @return Long 데이터 수
+ */
+ public Long countByBoardNoAndPostsNoAndUserId(Integer boardNo, Integer postsNo, String userId, String ipAddr) {
+ return jpaQueryFactory
+ .selectFrom(QPostsRead.postsRead)
+ .where(QPostsRead.postsRead.postsReadId.boardNo.eq(boardNo)
+ .and(QPostsRead.postsRead.postsReadId.postsNo.eq(postsNo))
+ .and(getBooleanExpression("userId", userId))
+ .and(getBooleanExpression("ipAddr", ipAddr)))
+ .fetchCount();
+ }
+
+ /**
+ * 다음 게시물 조회 번호 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Integer 다음 게시물 조회 번호
+ */
+ public Integer findNextReadNo(Integer boardNo, Integer postsNo) {
+ return jpaQueryFactory.select(QPostsRead.postsRead.postsReadId.readNo.max().add(1).coalesce(1))
+ .from(QPostsRead.postsRead)
+ .where(QPostsRead.postsRead.postsReadId.boardNo.eq(boardNo)
+ .and(QPostsRead.postsRead.postsReadId.postsNo.eq(postsNo)))
+ .fetchOne();
+ }
+
+ /**
+ * 엔티티 속성별 동적 검색 표현식 리턴
+ *
+ * @param attributeName 속성 명
+ * @param attributeValue 속성 값
+ * @return BooleanExpression 검색 표현식
+ */
+ private BooleanExpression getBooleanExpression(String attributeName, Object attributeValue) {
+ if (attributeValue == null || "".equals(attributeValue.toString())) return null;
+
+ switch (attributeName) {
+ case "userId": // 사용자 id
+ return QPostsRead.postsRead.userId.eq((String) attributeValue);
+ case "ipAddr": // ip 주소
+ return QPostsRead.postsRead.ipAddr.eq((String) attributeValue);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepository.java
new file mode 100644
index 0000000..0ed68b2
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepository.java
@@ -0,0 +1,24 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsRepository
+ *
+ * 게시물 레파지토리 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+public interface PostsRepository extends JpaRepository, PostsRepositoryCustom {
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryCustom.java
new file mode 100644
index 0000000..ad27f49
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryCustom.java
@@ -0,0 +1,102 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsRepositoryCustom
+ *
+ * 게시물 Querydsl 인터페이스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+public interface PostsRepositoryCustom {
+
+ /**
+ * 게시물 페이지 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param deleteAt 삭제 여부
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시물 목록 응답 DTO
+ */
+ Page findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable);
+
+ /**
+ * 게시판별 최근 게시물 목록 조회
+ *
+ * @param boardNos 게시판 번호 목록
+ * @param postsCount 게시물 수
+ * @return List 게시물 응답 DTO List
+ */
+ List findAllByBoardNosLimitCount(List boardNos, Integer postsCount);
+
+ /**
+ * 게시물 상세 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param userId 사용자 id
+ * @param ipAddr ip 주소
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ PostsResponseDto findById(Integer boardNo, Integer postsNo, String userId, String ipAddr);
+
+ /**
+ * 근처 게시물 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param gap 차이 -1: 이전, 1: 이후
+ * @param deleteAt 삭제 여부
+ * @param requestDto 요청 DTO
+ * @return List 게시물 상세 응답 DTO List
+ */
+ List findNearPost(Integer boardNo, Integer postsNo, long gap, Integer deleteAt, RequestDto requestDto);
+
+ /**
+ * 다음 게시물 번호 조회
+ *
+ * @param boardNo 게시판 번호
+ * @return Integer 다음 게시물 번호
+ */
+ Integer findNextPostsNo(Integer boardNo);
+
+ /**
+ * 게시물 조회 수 증가
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Long 처리 건수
+ */
+ Long updateReadCount(Integer boardNo, Integer postsNo);
+
+ /**
+ * 게시물 삭제 여부 수정
+ *
+ * @param posts 게시물 정보(게시판번호, 게시물번호배열)
+ * @param deleteAt 삭제 여부
+ * @param userId 사용자 id
+ * @return Long 처리 건수
+ */
+ Long updateDeleteAt(Map> posts, Integer deleteAt, String userId);
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryImpl.java
new file mode 100644
index 0000000..e525ed8
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryImpl.java
@@ -0,0 +1,406 @@
+package org.egovframe.cloud.boardservice.domain.posts;
+
+import java.time.LocalDateTime;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.egovframe.cloud.boardservice.api.board.dto.QBoardResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.QPostsListResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.QPostsResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.QPostsSimpleResponseDto;
+import org.egovframe.cloud.boardservice.domain.board.QBoard;
+import org.egovframe.cloud.boardservice.domain.comment.QComment;
+import org.egovframe.cloud.boardservice.domain.user.QUser;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+
+import com.google.common.base.CaseFormat;
+import com.querydsl.core.QueryResults;
+import com.querydsl.core.Tuple;
+import com.querydsl.core.types.ExpressionUtils;
+import com.querydsl.core.types.Order;
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.core.types.dsl.CaseBuilder;
+import com.querydsl.core.types.dsl.Expressions;
+import com.querydsl.core.types.dsl.NumberPath;
+import com.querydsl.core.types.dsl.SimpleExpression;
+import com.querydsl.core.types.dsl.StringPath;
+import com.querydsl.jpa.JPAExpressions;
+import com.querydsl.jpa.JPQLQuery;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.querydsl.sql.SQLExpressions;
+import com.querydsl.sql.SQLQuery;
+import com.querydsl.sql.SQLQueryFactory;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.posts.PostsRepositoryImpl
+ *
+ * 게시물 Querydsl 구현 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@RequiredArgsConstructor
+public class PostsRepositoryImpl implements PostsRepositoryCustom {
+
+ /**
+ * DML 생성을위한 Querydsl 팩토리 클래스
+ */
+ private final JPAQueryFactory jpaQueryFactory;
+
+ /**
+ * 쿼리 및 DML 절 생성을 위한 팩토리 클래스
+ */
+ private final SQLQueryFactory sqlQueryFactory;
+
+ /**
+ * 게시물 페이지 목록 조회
+ * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생
+ *
+ * @param boardNo 게시판 번호
+ * @param deleteAt 삭제 여부
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시물 목록 응답 DTO
+ */
+ @Override
+ public Page findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable) {
+ JPQLQuery query = jpaQueryFactory
+ .select(new QPostsListResponseDto(
+ QPosts.posts.postsId.boardNo,
+ QPosts.posts.postsId.postsNo,
+ QPosts.posts.postsTitle,
+ new CaseBuilder()
+ .when(QBoard.board.skinTypeCode.in("faq", "qna"))
+ .then(QPosts.posts.postsContent)
+ .otherwise(""),
+ new CaseBuilder()
+ .when(QBoard.board.skinTypeCode.in("faq", "qna"))
+ .then(QPosts.posts.postsAnswerContent)
+ .otherwise(""),
+ QPosts.posts.readCount,
+ QPosts.posts.noticeAt,
+ QPosts.posts.deleteAt,
+ QPosts.posts.createdBy,
+ QUser.user.userName.as("createdName"),
+ QPosts.posts.createdDate,
+ QBoard.board.newDisplayDayCount,
+ getCommentCountExpression(deleteAt)))
+ .from(QPosts.posts)
+ .innerJoin(QBoard.board).on(QPosts.posts.postsId.boardNo.eq(QBoard.board.boardNo))
+ .fetchJoin()
+ .leftJoin(QUser.user).on(QPosts.posts.createdBy.eq(QUser.user.userId))
+ .fetchJoin()
+ .where(QPosts.posts.postsId.boardNo.eq(boardNo)
+ .and(getBooleanExpression(requestDto.getKeywordType(), requestDto.getKeyword()))
+ .and(getBooleanExpression("deleteAt", deleteAt)));
+
+ //정렬
+ pageable.getSort().stream().forEach(sort -> {
+ Order order = sort.isAscending() ? Order.ASC : Order.DESC;
+ String property = sort.getProperty();
+ Path> parent;
+ if ("board_no".equals(property) || "posts_no".equals(property)) parent = QPosts.posts.postsId;
+ else parent = QPosts.posts;
+
+ Path target = Expressions.path(Object.class, parent, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property));
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ OrderSpecifier> orderSpecifier = new OrderSpecifier(order, target);
+ query.orderBy(orderSpecifier);
+ });
+
+ QueryResults result = query
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize()) //페이징
+ .fetchResults();
+
+ return new PageImpl<>(result.getResults(), pageable, result.getTotal());
+ }
+
+ /**
+ * 게시판별 최근 게시물 목록 조회
+ *
+ * JPQL 은 from 절에서 서브쿼리를 사용할 수 없어서 SQLQueryFactory 를 사용해 Native SQL 로 조회
+ * MySQL8 부터는 ROW_NUMBER, RANK 함수를 지원, 탬플릿에서는 MySQL5.7 로 개발해서 mysql 변수를 사용하는 방법으로 조회
+ * MySQL 문법이 포함되어있어서 다른 DBMS 를 사용하는 경우 수정 필요
+ *
+ * 인프런 김영한 강사는 추천하지 않는다.
+ * SqlQueryFactory는 저는 권장하지 않습니다. DB에서 메타데이터를 다 뽑아내서 생성해야 하는데... 너무 복잡하고 기능에 한계도 많습니다.
+ * 따라서 JPA와 JPA용 Querydsl을 최대한 사용하고, 그래도 잘 안되는 부분은 네이티브 쿼리를 사용하는 것이 더 좋다 생각합니다.
+ *
+ * 게시판별로 반복하여 게시물 조회하는 방법 등으로 JPQL 만을 사용해서 조회 가능
+ *
+ * @param boardNos 게시판 번호 목록
+ * @param postsCount 게시물 수
+ * @return List 게시물 응답 DTO List
+ */
+ @Override
+ public List findAllByBoardNosLimitCount(List boardNos, Integer postsCount) {
+ // path 정의
+ Path postsPath = Expressions.path(Posts.class, "posts");
+ NumberPath boardNoPath = Expressions.numberPath(Integer.class, postsPath, "board_no");
+ NumberPath postsNoPath = Expressions.numberPath(Integer.class, postsPath, "posts_no");
+
+ // 게시판번호, 로우넘 변수
+ StringPath varPath = Expressions.stringPath("v");
+ SQLQuery varSql = SQLExpressions.select(
+ Expressions.stringTemplate("@boardNo := 0"),
+ Expressions.stringTemplate("@rn := 0"));
+
+ // 게시물 조회
+ SQLQuery rownumSql = SQLExpressions
+ .select(boardNoPath,
+ postsNoPath,
+ Expressions.stringPath(postsPath, "posts_title"),
+ Expressions.stringPath(postsPath, "posts_content"),
+ Expressions.datePath(LocalDateTime.class, postsPath, "created_date"),
+ Expressions.stringTemplate("(CASE @boardNo WHEN posts.board_no THEN @rn := @rn + 1 ELSE @rn := 1 END)").as("rn"),
+ Expressions.stringTemplate("(@boardNo := posts.board_no)").as("boardNo"))
+ .from(QPosts.posts, postsPath)
+ .innerJoin(varSql, varPath)
+ .where(boardNoPath.in(boardNos)
+ .and(Expressions.numberPath(Integer.class, postsPath, "delete_at").eq(0)))
+ .orderBy(boardNoPath.asc(), postsNoPath.desc());
+
+ // 최근 게시물 조회
+ return sqlQueryFactory
+ .select(new QPostsSimpleResponseDto(boardNoPath,
+ postsNoPath,
+ Expressions.stringPath(postsPath, "posts_title"),
+ Expressions.stringPath(postsPath, "posts_content"),
+ Expressions.datePath(LocalDateTime.class, postsPath, "created_date")))
+ .from(rownumSql, postsPath)
+ .where(Expressions.numberPath(Integer.class, postsPath, "rn").loe(postsCount))
+ .fetch();
+ }
+
+ /**
+ * 게시물 상세 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param userId 사용자 id
+ * @param ipAddr ip 주소
+ * @return PostsResponseDto 게시물 상세 응답 DTO
+ */
+ @Override
+ public PostsResponseDto findById(Integer boardNo, Integer postsNo, String userId, String ipAddr) {
+ return jpaQueryFactory
+ .select(
+ // 게시물
+ new QPostsResponseDto(
+ QPosts.posts.postsId.boardNo,
+ QPosts.posts.postsId.postsNo,
+ QPosts.posts.postsTitle,
+ QPosts.posts.postsContent,
+ QPosts.posts.postsAnswerContent,
+ QPosts.posts.attachmentCode,
+ QPosts.posts.readCount,
+ QPosts.posts.noticeAt,
+ QPosts.posts.deleteAt,
+ QPosts.posts.createdBy,
+ QUser.user.userName.as("createdName"),
+ QPosts.posts.createdDate,
+ // 게시판
+ new QBoardResponseDto(QBoard.board.boardNo,
+ QBoard.board.boardName,
+ QBoard.board.skinTypeCode,
+ QBoard.board.titleDisplayLength,
+ QBoard.board.postDisplayCount,
+ QBoard.board.pageDisplayCount,
+ QBoard.board.newDisplayDayCount,
+ QBoard.board.editorUseAt,
+ QBoard.board.userWriteAt,
+ QBoard.board.commentUseAt,
+ QBoard.board.uploadUseAt,
+ QBoard.board.uploadLimitCount,
+ QBoard.board.uploadLimitSize),
+ // 댓글 수
+ // getCommentCountExpression(),
+ // 조회 사용자의 게시물 조회 수(조회 수 증가 확인 용)
+ ExpressionUtils.as(
+ JPAExpressions.select(ExpressionUtils.count(QPostsRead.postsRead.userId))
+ .from(QPostsRead.postsRead)
+ .where(QPostsRead.postsRead.postsReadId.boardNo.eq(QPosts.posts.postsId.boardNo)
+ .and(QPostsRead.postsRead.postsReadId.postsNo.eq(QPosts.posts.postsId.postsNo))
+ .and(getBooleanExpression("userId", userId))
+ .and(getBooleanExpression("ipAddr", ipAddr))),
+ "userPostsReadCount")))
+ .from(QPosts.posts) // 게시물
+ .innerJoin(QBoard.board).on(QPosts.posts.postsId.boardNo.eq(QBoard.board.boardNo)) // 게시판
+ .leftJoin(QUser.user).on(QPosts.posts.createdBy.eq(QUser.user.userId)) // 생성자
+ .fetchJoin()
+ .where(QPosts.posts.postsId.boardNo.eq(boardNo)
+ .and(QPosts.posts.postsId.postsNo.eq(postsNo)))
+ .fetchOne();
+ }
+
+ /**
+ * 이전 게시물 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param gap 차이 -1: 이전, 1: 이후
+ * @param deleteAt 삭제 여부
+ * @param requestDto 요청 DTO
+ * @return List 게시물 상세 응답 DTO List
+ */
+ @Override
+ public List findNearPost(Integer boardNo, Integer postsNo, long gap, Integer deleteAt, RequestDto requestDto) {
+ return jpaQueryFactory
+ .select(new QPostsSimpleResponseDto(
+ QPosts.posts.postsId.boardNo,
+ QPosts.posts.postsId.postsNo,
+ QPosts.posts.postsTitle,
+ QPosts.posts.postsContent,
+ QPosts.posts.createdDate))
+ .from(QPosts.posts)
+ .where(QPosts.posts.postsId.boardNo.eq(boardNo)
+ .and(getBooleanExpression(requestDto.getKeywordType(), requestDto.getKeyword()))
+ .and(getBooleanExpression("deleteAt", deleteAt))
+ .and(getBooleanExpression(gap < 0 ? "postsNoLt" : "postsNoGt", postsNo)))
+ .orderBy(gap < 0 ? QPosts.posts.noticeAt.asc() : QPosts.posts.noticeAt.desc(),
+ QPosts.posts.postsId.boardNo.asc(),
+ gap < 0 ? QPosts.posts.postsId.postsNo.desc() : QPosts.posts.postsId.postsNo.asc())
+ .limit((gap < 0 ? -1 : 1) * gap)
+ // .fetchFirst() // 단건 리턴
+ .fetch();
+ }
+
+ /**
+ * 다음 게시물 번호 조회
+ *
+ * @param boardNo 게시판 번호
+ * @return Integer 다음 게시물 번호
+ */
+ @Override
+ public Integer findNextPostsNo(Integer boardNo) {
+ return jpaQueryFactory
+ .select(QPosts.posts.postsId.postsNo.max().add(1).coalesce(1))
+ .from(QPosts.posts)
+ .where(QPosts.posts.postsId.boardNo.eq(boardNo))
+ .fetchOne();
+ }
+
+ /**
+ * 게시물 조회 수 증가
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Long 처리 건수
+ */
+ @Override
+ public Long updateReadCount(Integer boardNo, Integer postsNo) {
+ return jpaQueryFactory.update(QPosts.posts)
+ .set(QPosts.posts.readCount, QPosts.posts.readCount.add(1))
+ .where(QPosts.posts.postsId.boardNo.eq(boardNo)
+ .and(QPosts.posts.postsId.postsNo.eq(postsNo)))
+ .execute();
+ }
+
+ /**
+ * 게시물 삭제 여부 수정
+ *
+ * @param posts 게시물 정보(게시판번호, 게시물번호배열)
+ * @param deleteAt 삭제 여부
+ * @param userId 사용자 id
+ * @return Long 수정 건수
+ */
+ @Override
+ public Long updateDeleteAt(Map> posts, Integer deleteAt, String userId) {
+ long updateCount = 0L;
+
+ Iterator iterator = posts.keySet().iterator();
+ while (iterator.hasNext()) {
+ Integer boardNo = iterator.next();
+
+ List postsNoList = posts.get(boardNo);
+
+ updateCount += jpaQueryFactory.update(QPosts.posts)
+ .set(QPosts.posts.deleteAt, deleteAt)
+ .set(QPosts.posts.lastModifiedBy, userId)
+ .set(QPosts.posts.modifiedDate, LocalDateTime.now())
+ .where(QPosts.posts.postsId.boardNo.eq(boardNo)
+ .and(QPosts.posts.postsId.postsNo.in(postsNoList)))
+ .execute();
+ }
+
+ return updateCount;
+ }
+
+ /**
+ * 댓글 수 표현식
+ *
+ * @param deleteAt 삭제 여부
+ * @return SimpleExpression 댓글 수 표현식
+ */
+ private SimpleExpression getCommentCountExpression(Integer deleteAt) {
+ BooleanExpression deleteAtExpression = null;
+ if (deleteAt != null) {
+ deleteAtExpression = deleteAt == 0 ? QComment.comment.deleteAt.eq(0) : QComment.comment.deleteAt.ne(0);
+ }
+
+ return Expressions.as(new CaseBuilder()
+ .when(QBoard.board.commentUseAt.eq(true))
+ .then(JPAExpressions.select(ExpressionUtils.count(QComment.comment.commentId.commentNo))
+ .from(QComment.comment)
+ .where(QComment.comment.commentId.postsId.boardNo.eq(QPosts.posts.postsId.boardNo)
+ .and(QComment.comment.commentId.postsId.postsNo.eq(QPosts.posts.postsId.postsNo))
+ .and(QComment.comment.commentId.postsId.postsNo.eq(QPosts.posts.postsId.postsNo))
+ .and(deleteAtExpression)))
+ .otherwise(0L)
+ , "commentCount");
+ }
+
+ /**
+ * 엔티티 속성별 동적 검색 표현식 리턴
+ *
+ * @param attributeName 속성 명
+ * @param attributeValue 속성 값
+ * @return BooleanExpression 검색 표현식
+ */
+ private BooleanExpression getBooleanExpression(String attributeName, Object attributeValue) {
+ if (attributeValue == null || "".equals(attributeValue.toString())) return null;
+
+ switch (attributeName) {
+ case "userId": // 사용자 id
+ return QPostsRead.postsRead.userId.eq((String) attributeValue);
+ case "ipAddr": // ip 주소
+ return QPostsRead.postsRead.ipAddr.eq((String) attributeValue);
+ case "deleteAt": // 삭제 여부
+ return QPosts.posts.deleteAt.eq((Integer) attributeValue);
+ case "postsData": // 게시물 제목 + 내용
+ return QPosts.posts.postsTitle.containsIgnoreCase((String) attributeValue).or(QPosts.posts.postsContent.containsIgnoreCase((String) attributeValue));
+ case "postsTitle": // 게시물 제목
+ return QPosts.posts.postsTitle.containsIgnoreCase((String) attributeValue);
+ case "postsContent": // 게시물 내용
+ return QPosts.posts.postsContent.containsIgnoreCase((String) attributeValue);
+ case "postsNoLt": // 게시물 번호
+ return QPosts.posts.postsId.postsNo.lt((Integer) attributeValue);
+ case "postsNoGt": // 게시물 번호
+ return QPosts.posts.postsId.postsNo.gt((Integer) attributeValue);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/user/User.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/user/User.java
new file mode 100644
index 0000000..fa4a3b5
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/user/User.java
@@ -0,0 +1,49 @@
+package org.egovframe.cloud.boardservice.domain.user;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.egovframe.cloud.servlet.domain.BaseEntity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import java.io.Serializable;
+
+/**
+ * org.egovframe.cloud.boardservice.domain.user.User
+ *
+ * 사용자 정보 엔티티
+ *
+ * @author 표준프레임워크센터 jaeyeolkim
+ * @version 1.0
+ * @since 2021/06/30
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/06/30 jaeyeolkim 최초 생성
+ *
+ */
+@NoArgsConstructor
+@Entity
+@Getter
+public class User extends BaseEntity implements Serializable {
+
+ /**
+ * serialVersionUID
+ */
+ private static final long serialVersionUID = 8328774953218952698L;
+
+ @Id
+ @Column(name = "user_no", insertable = false, updatable = false)
+ private Long id;
+
+ @Column(insertable = false, updatable = false)
+ private String userId;
+
+ @Column(insertable = false, updatable = false)
+ private String userName;
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/board/BoardService.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/board/BoardService.java
new file mode 100644
index 0000000..2b0637f
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/board/BoardService.java
@@ -0,0 +1,136 @@
+package org.egovframe.cloud.boardservice.service.board;
+
+import lombok.RequiredArgsConstructor;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardSaveRequestDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardUpdateRequestDto;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.board.BoardRepository;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.egovframe.cloud.common.exception.EntityNotFoundException;
+import org.egovframe.cloud.common.service.AbstractService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * org.egovframe.cloud.boardservice.service.board.BoardService
+ *
+ * 게시판 서비스 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+@Service
+public class BoardService extends AbstractService {
+
+ /**
+ * 게시판 레파지토리 인터페이스
+ */
+ private final BoardRepository boardRepository;
+
+ /**
+ * 조회 조건에 일치하는 게시판 페이지 목록 조회
+ *
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시판 목록 응답 DTO
+ */
+ public Page findPage(RequestDto requestDto, Pageable pageable) {
+ return boardRepository.findPage(requestDto, pageable);
+ }
+
+ /**
+ * 게시판 목록 조회
+ *
+ * @param boardNos 게시판 번호 목록
+ * @return List 게시판 상세 응답 DTO List
+ */
+ public List findAllByBoardNos(List boardNos) {
+ return boardRepository.findAllByBoardNoIn(boardNos);
+ }
+
+ /**
+ * 게시판 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @return BoardResponseDto 게시판 응답 DTO
+ */
+ public BoardResponseDto findById(Integer boardNo) {
+ Board entity = findBoard(boardNo);
+
+ return new BoardResponseDto(entity);
+ }
+
+ /**
+ * 게시판 등록
+ *
+ * @param requestDto 게시판 등록 요청 DTO
+ * @return BoardResponseDto 게시판 응답 DTO
+ */
+ @Transactional
+ public BoardResponseDto save(BoardSaveRequestDto requestDto) {
+ Board entity = boardRepository.save(requestDto.toEntity());
+
+ return new BoardResponseDto(entity);
+ }
+
+ /**
+ * 게시판 수정
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 게시판 수정 요청 DTO
+ * @return BoardResponseDto 게시판 응답 DTO
+ */
+ @Transactional
+ public BoardResponseDto update(Integer boardNo, BoardUpdateRequestDto requestDto) {
+ Board entity = findBoard(boardNo);
+
+ // 수정
+ entity.update(requestDto.getBoardName(), requestDto.getSkinTypeCode(), requestDto.getTitleDisplayLength(), requestDto.getPostDisplayCount(),
+ requestDto.getPageDisplayCount(), requestDto.getNewDisplayDayCount(), requestDto.getEditorUseAt(), requestDto.getUserWriteAt(),
+ requestDto.getCommentUseAt(), requestDto.getUploadUseAt(), requestDto.getUploadLimitCount(), requestDto.getUploadLimitSize());
+
+ return new BoardResponseDto(entity);
+ }
+
+ /**
+ * 게시판 삭제
+ *
+ * @param boardNo 게시판 번호
+ */
+ @Transactional
+ public void delete(Integer boardNo) {
+ Board entity = findBoard(boardNo);
+
+ // 삭제
+ boardRepository.delete(entity);
+ }
+
+ /**
+ * 게시판 번호로 게시판 엔티티 조회
+ *
+ * @param boardNo 게시판 번호
+ * @return Board 게시판 엔티티
+ */
+ private Board findBoard(Integer boardNo) {
+ return boardRepository.findById(boardNo)
+ .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("board")})));
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/comment/CommentService.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/comment/CommentService.java
new file mode 100644
index 0000000..9a69710
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/comment/CommentService.java
@@ -0,0 +1,288 @@
+package org.egovframe.cloud.boardservice.service.comment;
+
+import lombok.RequiredArgsConstructor;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentUpdateRequestDto;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.comment.Comment;
+import org.egovframe.cloud.boardservice.domain.comment.CommentId;
+import org.egovframe.cloud.boardservice.domain.comment.CommentRepository;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+import org.egovframe.cloud.boardservice.service.posts.PostsService;
+import org.egovframe.cloud.common.exception.BusinessMessageException;
+import org.egovframe.cloud.common.exception.EntityNotFoundException;
+import org.egovframe.cloud.common.exception.InvalidValueException;
+import org.egovframe.cloud.common.service.AbstractService;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * org.egovframe.cloud.boardservice.service.comment.CommentService
+ *
+ * 댓글 서비스 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/04
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/04 jooho 최초 생성
+ *
+ */
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+@Service
+public class CommentService extends AbstractService {
+
+ /**
+ * 게시물 레파지토리 인터페이스
+ */
+ private final CommentRepository commentRepository;
+
+ /**
+ * 게시물 서비스
+ */
+ private final PostsService postsService;
+
+ /**
+ * 게시글의 댓글 전체 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ public Map findAll(Integer boardNo, Integer postsNo) {
+ return findAll(boardNo, postsNo, null);
+ }
+
+ /**
+ * 게시글의 댓글 전체 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param deleteAt 삭제 여부
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ public Map findAll(Integer boardNo, Integer postsNo, Integer deleteAt) {
+ List comments = commentRepository.findAll(boardNo, postsNo, deleteAt);
+
+ // 페이지 인터페이스와 동일한 속성의 맵 리턴
+ Map result = new HashMap<>();
+
+ result.put("content", comments);
+ result.put("empty", comments.isEmpty());
+ result.put("first", true);
+ result.put("last", true);
+ result.put("number", 0);
+ result.put("numberOfElements", comments.size());
+ result.put("size", comments.size());
+ result.put("totalElements", comments.size());
+ result.put("totalPages", 1);
+
+ return result;
+ }
+
+ /**
+ * 게시글의 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param pageable 페이지 정보
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ public Map findPage(Integer boardNo, Integer postsNo, Pageable pageable) {
+ return commentRepository.findPage(boardNo, postsNo, null, pageable);
+ }
+
+ /**
+ * 게시글의 댓글 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param deleteAt 삭제 여부
+ * @param pageable 페이지 정보
+ * @return Map 페이지 댓글 목록 응답 DTO
+ */
+ public Map findPage(Integer boardNo, Integer postsNo, Integer deleteAt, Pageable pageable) {
+ return commentRepository.findPage(boardNo, postsNo, deleteAt, pageable);
+ }
+
+ /**
+ * 댓글 등록
+ *
+ * @param requestDto 댓글 등록 요청 DTO
+ */
+ @Transactional
+ public CommentResponseDto save(CommentSaveRequestDto requestDto) {
+ if (requestDto.getBoardNo() == null || requestDto.getPostsNo() == null) {
+ throw new InvalidValueException(getMessage("err.invalid.input.value"));
+ }
+
+ Posts posts = postsService.findPosts(requestDto.getBoardNo(), requestDto.getPostsNo());
+ checkEditableComment(posts); // 저장 가능 여부 확인
+
+ Integer sortSeq;
+ if (requestDto.getParentCommentNo() != null) { // 대댓글
+ sortSeq = commentRepository.findNextSortSeq(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getGroupNo(), requestDto.getParentCommentNo(), requestDto.getDepthSeq());
+ if (sortSeq != null) {
+ commentRepository.updateSortSeq(requestDto.getGroupNo(), sortSeq, null, 1); // 들어갈 위치와 같거나 큰 대댓글 정렬 순서 +1
+ } else {
+ sortSeq = commentRepository.findLastSortSeq(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getGroupNo()); // 들어갈 위치가 검색되지 않으면 max
+ }
+ } else {
+ sortSeq = 1;
+ }
+
+ Integer commentNo = commentRepository.findNextCommentNo(requestDto.getBoardNo(), requestDto.getPostsNo()); // 댓글 번호 채번
+ Integer groupNo; // 댓글 그룹 번호
+ if (requestDto.getGroupNo() != null) groupNo = requestDto.getGroupNo();
+ else groupNo = commentNo; // 최상위 댓글
+
+ Comment entity = commentRepository.save(requestDto.toEntity(posts, commentNo, groupNo, sortSeq));
+
+ return new CommentResponseDto(entity);
+ }
+
+ /**
+ * 댓글 수정(작성자 체크)
+ *
+ * @param requestDto 댓글 수정 요청 DTO
+ */
+ @Transactional
+ public CommentResponseDto update(CommentUpdateRequestDto requestDto, String userId) {
+ Comment entity = findCommentByCreatedBy(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getCommentNo(), userId);
+
+ checkEditableComment(entity.getPosts()); // 저장 가능 여부 확인
+
+ // 수정
+ entity.update(requestDto.getCommentContent());
+
+ return new CommentResponseDto(entity);
+ }
+
+ /**
+ * 댓글 삭제(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ */
+ @Transactional
+ public void delete(Integer boardNo, Integer postsNo, Integer commentNo, String userId) {
+ Comment entity = findCommentByCreatedBy(boardNo, postsNo, commentNo, userId);
+
+ checkEditableComment(entity.getPosts()); // 변경 가능 여부 확인
+
+ entity.updateDeleteAt(1); // 작성자 삭제
+ }
+
+ /**
+ * 댓글 수정
+ *
+ * @param requestDto 댓글 수정 요청 DTO
+ */
+ @Transactional
+ public CommentResponseDto update(CommentUpdateRequestDto requestDto) {
+ Comment entity = findComment(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getCommentNo());
+
+ // 수정
+ entity.update(requestDto.getCommentContent());
+
+ return new CommentResponseDto(entity);
+ }
+
+ /**
+ * 댓글 삭제
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ */
+ @Transactional
+ public void delete(Integer boardNo, Integer postsNo, Integer commentNo) {
+ Comment entity = findComment(boardNo, postsNo, commentNo);
+
+ entity.updateDeleteAt(2); // 관리자 삭제 - 관리자가 본인 댓글 지울 경우 관리자 삭제로 처리
+ }
+
+ /**
+ * 댓글 기본키로 댓글 엔티티 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ * @return Comment 댓글 엔티티
+ */
+ private Comment findComment(Integer boardNo, Integer postsNo, Integer commentNo) {
+ if (boardNo == null || postsNo == null || commentNo == null) {
+ throw new InvalidValueException(getMessage("err.invalid.input.value"));
+ }
+
+ CommentId id = CommentId.builder()
+ .postsId(PostsId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .build())
+ .commentNo(commentNo)
+ .build();
+
+ return commentRepository.findById(id)
+ .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("comment")}))); // 게시물이(가) 없습니다.
+ }
+
+ /**
+ * 댓글 기본키로 댓글 엔티티 조회
+ * 작성자 체크하여 본인 댓글이 아닌 경우 예외 발생
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ * @param userId 사용자 id
+ * @return Comment 댓글 엔티티
+ */
+ private Comment findCommentByCreatedBy(Integer boardNo, Integer postsNo, Integer commentNo, String userId) {
+ if (userId == null) {
+ throw new BusinessMessageException(getMessage("err.required.login")); // 로그인 후 다시 시도해주세요.
+ }
+
+ Comment entity = findComment(boardNo, postsNo, commentNo);
+
+ if (!userId.equals(entity.getCreatedBy())) {
+ throw new BusinessMessageException(getMessage("err.unauthorized")); // 권한이 불충분합니다
+ }
+
+ return entity;
+ }
+
+ /**
+ * 댓글 등록/수정/삭제 가능 여부 확인
+ * 댓글 사용 여부, 게시물 삭제 여부 체크해서 예외 발생
+ *
+ * @param posts 게시물 엔티티
+ */
+ private void checkEditableComment(Posts posts) {
+ Board board = posts.getBoard();
+ if (board == null) {
+ throw new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("board")})); // 게시판이(가) 없습니다.
+ }
+ if (Boolean.FALSE.equals(board.getCommentUseAt())) {
+ throw new BusinessMessageException(getMessage("err.board.not_use_comment")); // 댓글 사용이 금지된 게시판입니다.
+ }
+ if (posts.getDeleteAt().compareTo(0) > 0) {
+ throw new BusinessMessageException(getMessage("err.posts.deleted")); // 삭제된 게시물입니다.
+ }
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/posts/PostsService.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/posts/PostsService.java
new file mode 100644
index 0000000..80cb705
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/posts/PostsService.java
@@ -0,0 +1,411 @@
+package org.egovframe.cloud.boardservice.service.posts;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsDeleteRequestDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsSaveRequestDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleSaveRequestDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsUpdateRequestDto;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+import org.egovframe.cloud.boardservice.domain.posts.PostsRead;
+import org.egovframe.cloud.boardservice.domain.posts.PostsReadRepository;
+import org.egovframe.cloud.boardservice.domain.posts.PostsRepository;
+import org.egovframe.cloud.boardservice.service.board.BoardService;
+import org.egovframe.cloud.common.dto.AttachmentEntityMessage;
+import org.egovframe.cloud.common.dto.RequestDto;
+import org.egovframe.cloud.common.exception.BusinessMessageException;
+import org.egovframe.cloud.common.exception.EntityNotFoundException;
+import org.egovframe.cloud.common.exception.InvalidValueException;
+import org.egovframe.cloud.common.service.AbstractService;
+import org.springframework.cloud.stream.function.StreamBridge;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * org.egovframe.cloud.postsservice.service.posts.PostsService
+ *
+ * 게시물 서비스 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/28
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/28 jooho 최초 생성
+ *
+ */
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+@Service
+public class PostsService extends AbstractService {
+
+ /**
+ * 게시물 레파지토리 인터페이스
+ */
+ private final PostsRepository postsRepository;
+
+ /**
+ * 게시물 조회 레파지토리 인터페이스
+ */
+ private final PostsReadRepository postsReadRepository;
+
+ /**
+ * 게시판 서비스 클래스
+ */
+ private final BoardService boardService;
+
+ /**
+ * 이벤트 메시지 발행하기 위한 spring cloud stream 유틸리티 클래스
+ */
+ private final StreamBridge streamBridge;
+
+ /**
+ * 조회 조건에 일치하는 게시물 페이지 목록 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param deleteAt 삭제 여부
+ * @param requestDto 요청 DTO
+ * @param pageable 페이지 정보
+ * @return Page 페이지 게시물 목록 응답 DTO
+ */
+ public Page findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable) {
+ if (boardNo == null || boardNo <= 0) throw new InvalidValueException(getMessage("err.invalid.input.value"));
+ return postsRepository.findPage(boardNo, deleteAt, requestDto, pageable);
+ }
+
+ /**
+ * 최근 게시물이 포함된 게시판 목록 조회
+ *
+ * @param boardNos 게시판 번호 목록
+ * @param postsCount 게시물 수
+ * @return Map 최근 게시물이 포함된 게시판 상세 응답 DTO Map
+ */
+ public Map findNewest(List boardNos, Integer postsCount) {
+ if (boardNos == null || boardNos.isEmpty())
+ throw new InvalidValueException(getMessage("err.invalid.input.value"));
+
+ List boards = boardService.findAllByBoardNos(boardNos);
+
+ List allPosts = postsRepository.findAllByBoardNosLimitCount(boardNos, postsCount);
+ Map> postsGroup = allPosts.stream().collect(Collectors.groupingBy(PostsSimpleResponseDto::getBoardNo, Collectors.toList()));
+
+ Map data = new HashMap<>(); // 요청한 게시판 순서로 리턴하기 위해서 map 리턴
+ for (BoardResponseDto board : boards) {
+ List posts = postsGroup.get(board.getBoardNo())
+ .stream().map(post -> post.setIsNew(board))
+ .collect(Collectors.toList());
+ board.setNewestPosts(posts);
+ data.put(board.getBoardNo(), board);
+ }
+
+ return data;
+ }
+
+ /**
+ * 게시물 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param userId 사용자 id
+ * @param ipAddr ip 주소
+ * @return PostsResponseDto 게시물 응답 DTO
+ */
+ @Transactional
+ public PostsResponseDto findById(Integer boardNo, Integer postsNo, Integer deleteAt, String userId, String ipAddr) {
+ return findById(boardNo, postsNo, deleteAt, userId, ipAddr, null);
+ }
+
+ /**
+ * 게시물 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param deleteAt 삭제 여부
+ * @param userId 사용자 id
+ * @param ipAddr ip 주소
+ * @param requestDto 요청 DTO
+ * @return PostsResponseDto 게시물 응답 DTO
+ */
+ @Transactional
+ public PostsResponseDto findById(Integer boardNo, Integer postsNo, Integer deleteAt, String userId, String ipAddr, RequestDto requestDto) {
+ PostsResponseDto dto = postsRepository.findById(boardNo, postsNo, userId, ipAddr);
+
+ if (dto == null) {
+ throw new EntityNotFoundException("not found posts : "+ boardNo + ", " + postsNo + ", " + userId + ", " + ipAddr);
+ }
+
+ // 삭제 여부 확인
+ if (deleteAt != null && deleteAt.intValue() != dto.getDeleteAt().intValue()) {
+ throw new BusinessMessageException(getMessage("err.posts.deleted"));
+ }
+
+ if (dto.getUserPostsReadCount() == 0) {
+ // 게시판 조회 등록
+ Integer readNo = postsReadRepository.findNextReadNo(boardNo, postsNo);
+ PostsRead postsRead = PostsRead.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .readNo(readNo)
+ .userId(userId)
+ .ipAddr(ipAddr)
+ .build();
+
+ postsReadRepository.save(postsRead);
+
+ // 조회 수 증가
+ postsRepository.updateReadCount(boardNo, postsNo);
+
+ // dto 조회 수 증가
+ dto.increaseReadCount();
+ }
+
+ // 이전글, 다음글 조회
+ if (requestDto != null) {
+ List prevPosts = postsRepository.findNearPost(boardNo, postsNo, -1, deleteAt, requestDto);
+ dto.setPrevPosts(prevPosts);
+ List nextPosts = postsRepository.findNearPost(boardNo, postsNo, 1, deleteAt, requestDto);
+ dto.setNextPosts(nextPosts);
+ }
+
+ return dto;
+ }
+
+ /**
+ * 게시물 등록
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 게시물 등록 요청 DTO
+ * @return PostsResponseDto 게시물 응답 DTO
+ */
+ @Transactional
+ public PostsResponseDto save(Integer boardNo, PostsSaveRequestDto requestDto) {
+ Integer postsNo = postsRepository.findNextPostsNo(boardNo);
+
+ Posts entity = postsRepository.save(requestDto.toEntity(boardNo, postsNo));
+
+ /**
+ * 첨부파일 entity 정보 저장 이벤트 발생
+ */
+ sendAttachmentEvent(entity);
+
+ return new PostsResponseDto(entity);
+ }
+
+ /**
+ * 게시물 수정(권한 체크 안함)
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param requestDto 게시물 수정 요청 DTO
+ * @return PostsResponseDto 게시물 응답 DTO
+ */
+ @Transactional
+ public PostsResponseDto update(Integer boardNo, Integer postsNo, PostsUpdateRequestDto requestDto) {
+ Posts entity = findPosts(boardNo, postsNo);
+
+ // 수정
+ entity.update(requestDto.getPostsTitle(), requestDto.getPostsContent(), requestDto.getPostsAnswerContent(), requestDto.getAttachmentCode(), requestDto.getNoticeAt());
+
+ return new PostsResponseDto(entity);
+ }
+
+ /**
+ * 게시물 다건 삭제(권한 체크 안함)
+ *
+ * @param requestDtoList 게시물 삭제 요청 DTO List
+ * @param userId 사용자 id
+ * @return Long 삭제 건수
+ */
+ @Transactional
+ public Long remove(List requestDtoList, String userId) {
+ Map> data = requestDtoList.stream()
+ .collect(Collectors.groupingBy(PostsDeleteRequestDto::getBoardNo,
+ Collectors.mapping(PostsDeleteRequestDto::getPostsNo, Collectors.toList())));
+
+ return postsRepository.updateDeleteAt(data, 2, userId);
+ }
+
+ /**
+ * 게시물 다건 복원(권한 체크 안함)
+ *
+ * @param requestDtoList 게시물 삭제 요청 DTO List
+ * @param userId 사용자 id
+ * @return Long 복원 건수
+ */
+ @Transactional
+ public Long restore(List requestDtoList, String userId) {
+ Map> data = requestDtoList.stream()
+ .collect(Collectors.groupingBy(PostsDeleteRequestDto::getBoardNo,
+ Collectors.mapping(PostsDeleteRequestDto::getPostsNo, Collectors.toList())));
+
+ return postsRepository.updateDeleteAt(data, 0, userId);
+ }
+
+ /**
+ * 게시물 다건 완전 삭제(권한 체크 안함)
+ *
+ * @param requestDtoList 게시물 삭제 요청 DTO List
+ */
+ @Transactional
+ public void delete(List requestDtoList) {
+ List deleteEntityList = requestDtoList.stream()
+ .map(PostsDeleteRequestDto::toEntity)
+ .collect(Collectors.toList());
+
+ // 일괄 처리
+ postsRepository.deleteAll(deleteEntityList);
+ }
+
+ /**
+ * 게시물 등록(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param requestDto 게시물 등록 요청 DTO
+ * @return PostsResponseDto 게시물 응답 DTO
+ */
+ @Transactional
+ public PostsResponseDto save(Integer boardNo, PostsSimpleSaveRequestDto requestDto, String userId) {
+ checkUserWritable(boardNo);
+
+ Integer postsNo = postsRepository.findNextPostsNo(boardNo);
+
+ Posts entity = postsRepository.save(requestDto.toEntity(boardNo, postsNo));
+
+ /**
+ * 첨부파일 entity 정보 저장 이벤트 발생
+ */
+ sendAttachmentEvent(entity);
+
+ return new PostsResponseDto(entity);
+ }
+
+ /**
+ * 게시물 수정(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param requestDto 게시물 수정 요청 DTO
+ * @param userId 사용자 id
+ * @return PostsResponseDto 게시물 응답 DTO
+ */
+ @Transactional
+ public PostsResponseDto update(Integer boardNo, Integer postsNo, PostsSimpleSaveRequestDto requestDto, String userId) {
+ Posts entity = findPostsByCreatedBy(boardNo, postsNo, userId);
+
+ // 수정
+ entity.update(requestDto.getPostsTitle(), requestDto.getPostsContent(), requestDto.getAttachmentCode());
+
+ return new PostsResponseDto(entity);
+ }
+
+ /**
+ * 게시물 삭제(작성자 체크)
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param userId 사용자 id
+ */
+ @Transactional
+ public void remove(Integer boardNo, Integer postsNo, String userId) {
+ Posts entity = findPostsByCreatedBy(boardNo, postsNo, userId);
+
+ // 삭제 여부 수정
+ entity.updateDeleteAt(1);
+ }
+
+ /**
+ * 게시물 번호로 게시물 엔티티 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @return Posts 게시물 엔티티
+ * @throws InvalidValueException 입력값 예외
+ * @throws EntityNotFoundException 엔티티 예외
+ */
+ public Posts findPosts(Integer boardNo, Integer postsNo) throws InvalidValueException, EntityNotFoundException {
+ if (boardNo == null || postsNo == null) {
+ throw new InvalidValueException(getMessage("err.invalid.input.value"));
+ }
+
+ PostsId id = PostsId.builder().boardNo(boardNo).postsNo(postsNo).build();
+
+ return postsRepository.findById(id)
+ .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("posts")})));
+ }
+
+ /**
+ * 게시물 번호, 작성자로 게시물 엔티티 조회
+ * 로그인 확인
+ * 로그인 사용자가 작성자인지 확인
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param userId 사용자 id
+ * @return Posts 게시물 엔티티
+ * @throws BusinessMessageException 비지니스 예외
+ */
+ private Posts findPostsByCreatedBy(Integer boardNo, Integer postsNo, String userId) throws BusinessMessageException {
+ if (userId == null || "".equals(userId)) {
+ throw new BusinessMessageException(getMessage("err.required.login")); // 로그인 후 다시 시도해주세요.
+ }
+
+ Posts entity = findPosts(boardNo, postsNo);
+
+ if (!userId.equals(entity.getCreatedBy())) {
+ throw new BusinessMessageException(getMessage("err.unauthorized")); // 권한이 불충분합니다
+ }
+
+ checkUserWritable(boardNo);
+
+ return entity;
+ }
+
+ /**
+ * 게시판 사용자 작성 여부 확인
+ *
+ * @param boardNo 게시판 번호
+ * @throws BusinessMessageException 비지니스 예외
+ */
+ private void checkUserWritable(Integer boardNo) throws BusinessMessageException {
+ BoardResponseDto board = boardService.findById(boardNo);
+ if (!Boolean.TRUE.equals(board.getUserWriteAt())) {
+ // 게시판 작성 가능 여부를 알려줄 필요는 없다.
+ throw new BusinessMessageException(getMessage("err.unauthorized")); // 권한이 불충분합니다.
+ }
+ }
+
+ /**
+ * 첨부파일 entity 정보 업데이트 하기 위해 이벤트 메세지 발행
+ *
+ * @param entity
+ */
+ private void sendAttachmentEvent(Posts entity) {
+ if (!StringUtils.hasText(entity.getAttachmentCode())) {
+ return;
+ }
+ sendAttachmentEntityInfo(streamBridge,
+ AttachmentEntityMessage.builder()
+ .attachmentCode(entity.getAttachmentCode())
+ .entityName(entity.getClass().getName())
+ .entityId(entity.getPostsId().toString())
+ .build());
+ }
+
+}
diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/util/HttpUtil.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/util/HttpUtil.java
new file mode 100644
index 0000000..b30c1ae
--- /dev/null
+++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/util/HttpUtil.java
@@ -0,0 +1,42 @@
+package org.egovframe.cloud.boardservice.util;
+
+import org.egovframe.cloud.common.domain.Role;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+/**
+ * org.egovframe.cloud.boardservice.util.HttpUtil
+ *
+ * HTTP 관련 유틸 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/09/09
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/09/09 jooho 최초 생성
+ *
+ */
+public class HttpUtil {
+
+ /**
+ * static method 만으로 구성된 유틸리티 클래스
+ * 객체 생성 금지
+ */
+ private HttpUtil() {
+ throw new IllegalStateException("Http Utility Class");
+ }
+
+ /***
+ * 관리자 권한 확인
+ * @return boolean 관리자 여부
+ */
+ public static boolean isAdmin() {
+ return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
+ .anyMatch(r -> r.toString().equals(Role.ADMIN.getKey()));
+ }
+
+}
diff --git a/backend/board-service/src/main/resources/application.yml b/backend/board-service/src/main/resources/application.yml
new file mode 100644
index 0000000..50ced93
--- /dev/null
+++ b/backend/board-service/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+server:
+ port: 0 # random port
+
+spring:
+ application:
+ name: board-service
+ jpa:
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.MySQL57Dialect
+ storage_engine: innodb
+ format_sql: true
+ default_batch_fetch_size: 1000
+ show-sql: true
+ servlet:
+ multipart:
+ enabled: true
+ max-file-size: 10MB
+ max-request-size: 50MB
+
+# config server actuator
+management:
+ endpoints:
+ web:
+ exposure:
+ include: refresh, health, beans
diff --git a/backend/board-service/src/main/resources/bootstrap.yml b/backend/board-service/src/main/resources/bootstrap.yml
new file mode 100644
index 0000000..be89492
--- /dev/null
+++ b/backend/board-service/src/main/resources/bootstrap.yml
@@ -0,0 +1,8 @@
+spring:
+ cloud:
+ config:
+ uri: http://localhost:8888
+ name: board-service # board-service.yml이 있으면 불러오게 된다
+# name: config-service # config-service의 application.yml 을 불러오게 된다
+# profiles:
+# active: prod # application-prod.yml
\ No newline at end of file
diff --git a/backend/board-service/src/main/resources/logback-spring.xml b/backend/board-service/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..03aa0f4
--- /dev/null
+++ b/backend/board-service/src/main/resources/logback-spring.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${destination}
+
+ {"app.name":"${app_name}"}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/board/BoardApiControllerTest.java b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/board/BoardApiControllerTest.java
new file mode 100644
index 0000000..78a0e02
--- /dev/null
+++ b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/board/BoardApiControllerTest.java
@@ -0,0 +1,440 @@
+package org.egovframe.cloud.boardservice.api.board;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.assertj.core.api.Condition;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto;
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.board.BoardRepository;
+import org.egovframe.cloud.boardservice.util.RestResponsePage;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * org.egovframe.cloud.boardservice.api.board.BoardApiControllerTest
+ *
+ * 게시판 Rest API 컨트롤러 테스트 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/07/26
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/07/26 jooho 최초 생성
+ *
+ */
+@Slf4j
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@EnableConfigurationProperties
+@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
+@ActiveProfiles(profiles = "test")
+public class BoardApiControllerTest {
+
+ /**
+ * test rest template
+ */
+ @Autowired
+ TestRestTemplate restTemplate;
+
+ /**
+ * 게시판 레파지토리 인터페이스
+ */
+ @Autowired
+ BoardRepository boardRepository;
+
+ /**
+ * 게시판 API 경로
+ */
+ private static final String URL = "/api/v1/boards";
+
+ /**
+ * 테스트 데이터 등록 횟수
+ */
+ private final Integer GIVEN_DATA_COUNT = 10;
+
+ /**
+ * 테스트 데이터
+ */
+ private final String BOARD_NAME_PREFIX = "게시판 명";
+ private final String SKIN_TYPE_CODE_PREFIX = "000";
+
+ private final Integer BOARD_NO = GIVEN_DATA_COUNT + 1;
+ private final String INSERT_BOARD_NAME = BOARD_NAME_PREFIX + "_" + BOARD_NO;
+ private final String INSERT_SKIN_TYPE_CODE = SKIN_TYPE_CODE_PREFIX + "_" + BOARD_NO;
+ private final Integer INSERT_TITLE_DISPLAY_LENGTH = 50;
+ private final Integer INSERT_POST_DISPLAY_COUNT = 10;
+ private final Integer INSERT_PAGE_DISPLAY_COUNT = 10;
+ private final Integer INSERT_NEW_DISPLAY_COUNT = 3;
+ private final Boolean INSERT_EDITOR_USE_AT = true;
+ private final Boolean INSERT_UPLOAD_USE_AT = true;
+ private final Boolean INSERT_USER_WRITE_AT = true;
+ private final Boolean INSERT_COMMENT_USE_AT = true;
+ private final Integer INSERT_UPLOAD_FILE_COUNT = 5;
+ private final BigDecimal INSERT_UPLOAD_LIMIT_SIZE = new BigDecimal("104857600");
+
+ private final String UPDATE_BOARD_NAME = BOARD_NAME_PREFIX + "_" + (BOARD_NO + 1);
+ private final String UPDATE_SKIN_TYPE_CODE = SKIN_TYPE_CODE_PREFIX + "_" + (BOARD_NO + 1);
+ private final Integer UPDATE_TITLE_DISPLAY_LENGTH = 50 + 1;
+ private final Integer UPDATE_POST_DISPLAY_COUNT = 10 + 1;
+ private final Integer UPDATE_PAGE_DISPLAY_COUNT = 10 + 1;
+ private final Integer UPDATE_NEW_DISPLAY_COUNT = 3 + 1;
+ private final Boolean UPDATE_EDITOR_USE_AT = false;
+ private final Boolean UPDATE_UPLOAD_USE_AT = false;
+ private final Boolean UPDATE_USER_WRITE_AT = false;
+ private final Boolean UPDATE_COMMENT_USE_AT = false;
+ private final Integer UPDATE_UPLOAD_FILE_COUNT = 5 + 1;
+ private final BigDecimal UPDATE_UPLOAD_LIMIT_SIZE = new BigDecimal("209715200");
+
+ /**
+ * 테스트 시작 전 수행
+ */
+ @BeforeEach
+ void setUp() {
+ log.info("###setUp");
+ }
+
+ /**
+ * 테스트 종료 후 수행
+ */
+ @AfterEach
+ void tearDown() {
+ log.info("###tearDown");
+
+ //게시판 삭제
+ boardRepository.deleteAll();
+ }
+
+ /**
+ * 게시판 페이지 목록 조회 테스트
+ */
+ @Test
+ void 게시판_페이지_목록_조회() {
+ log.info("###게시판_페이지_목록_조회");
+
+ // given
+ insertBoards();
+
+ String queryString = "?keywordType=boardName&keyword=" + BOARD_NAME_PREFIX; // 검색 조건
+ queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보
+
+ // when
+ ResponseEntity> responseEntity = restTemplate.exchange(
+ URL + queryString,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ RestResponsePage page = responseEntity.getBody();
+ assertThat(page).isNotNull();
+ assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT);
+ assertThat(page.getContent())
+ .isNotEmpty()
+ .has(new Condition<>(l -> (BOARD_NAME_PREFIX + "_10").equals(l.get(0).getBoardName()), "BoardApiControllerTest.findPage contains " + BOARD_NAME_PREFIX + "_10"))
+ .has(new Condition<>(l -> (BOARD_NAME_PREFIX + "_9").equals(l.get(1).getBoardName()), "BoardApiControllerTest.findPage contains " + BOARD_NAME_PREFIX + "_9"));
+ }
+
+ /**
+ * 게시판 상세 조회 테스트
+ */
+ @Test
+ void 게시판_상세_조회() {
+ log.info("###게시판_상세_조회");
+
+ // given
+ Board entity = insertBoard();
+
+ final Integer boardNo = entity.getBoardNo();
+
+ String url = URL + "/" + boardNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ BoardResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+ assertThat(dto.getBoardNo()).isEqualTo(boardNo);
+ assertThat(dto.getBoardName()).isEqualTo(INSERT_BOARD_NAME);
+ assertThat(dto.getSkinTypeCode()).isEqualTo(INSERT_SKIN_TYPE_CODE);
+ assertThat(dto.getTitleDisplayLength()).isEqualTo(INSERT_TITLE_DISPLAY_LENGTH);
+ assertThat(dto.getPostDisplayCount()).isEqualTo(INSERT_POST_DISPLAY_COUNT);
+ assertThat(dto.getPageDisplayCount()).isEqualTo(INSERT_PAGE_DISPLAY_COUNT);
+ assertThat(dto.getNewDisplayDayCount()).isEqualTo(INSERT_NEW_DISPLAY_COUNT);
+ assertThat(dto.getEditorUseAt()).isEqualTo(INSERT_EDITOR_USE_AT);
+ assertThat(dto.getUploadUseAt()).isEqualTo(INSERT_UPLOAD_USE_AT);
+ assertThat(dto.getUserWriteAt()).isEqualTo(INSERT_USER_WRITE_AT);
+ assertThat(dto.getCommentUseAt()).isEqualTo(INSERT_COMMENT_USE_AT);
+ assertThat(dto.getUploadLimitCount()).isEqualTo(INSERT_UPLOAD_FILE_COUNT);
+ assertThat(dto.getUploadLimitSize()).isEqualTo(INSERT_UPLOAD_LIMIT_SIZE);
+
+ deleteBoard(boardNo);
+ }
+
+ /**
+ * 게시판 등록 테스트
+ */
+ @Test
+ void 게시판_등록() {
+ log.info("###게시판_등록");
+
+ // given
+ Map params = new HashMap<>();
+ params.put("boardName", INSERT_BOARD_NAME);
+ params.put("skinTypeCode", INSERT_SKIN_TYPE_CODE);
+ params.put("titleDisplayLength", INSERT_TITLE_DISPLAY_LENGTH);
+ params.put("postDisplayCount", INSERT_POST_DISPLAY_COUNT);
+ params.put("pageDisplayCount", INSERT_PAGE_DISPLAY_COUNT);
+ params.put("newDisplayDayCount", INSERT_NEW_DISPLAY_COUNT);
+ params.put("editorUseAt", INSERT_EDITOR_USE_AT);
+ params.put("uploadUseAt", INSERT_UPLOAD_USE_AT);
+ params.put("userWriteAt", INSERT_USER_WRITE_AT);
+ params.put("commentUseAt", INSERT_COMMENT_USE_AT);
+ params.put("uploadLimitCount", INSERT_UPLOAD_FILE_COUNT);
+ params.put("uploadLimitSize", INSERT_UPLOAD_LIMIT_SIZE);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ // when
+ //ResponseEntity responseEntity = restTemplate.postForEntity(URL, requestDto, PostsResponseDto.class);
+ ResponseEntity responseEntity = restTemplate.exchange(
+ URL,
+ HttpMethod.POST,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ BoardResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+
+ final Integer boardNo = dto.getBoardNo();
+
+ Optional board = selectData(boardNo);
+ assertThat(board).isPresent();
+
+ Board entity = board.get();
+ assertThat(entity.getBoardNo()).isEqualTo(boardNo);
+ assertThat(entity.getBoardName()).isEqualTo(INSERT_BOARD_NAME);
+ assertThat(entity.getSkinTypeCode()).isEqualTo(INSERT_SKIN_TYPE_CODE);
+ assertThat(entity.getTitleDisplayLength()).isEqualTo(INSERT_TITLE_DISPLAY_LENGTH);
+ assertThat(entity.getPostDisplayCount()).isEqualTo(INSERT_POST_DISPLAY_COUNT);
+ assertThat(entity.getPageDisplayCount()).isEqualTo(INSERT_PAGE_DISPLAY_COUNT);
+ assertThat(entity.getNewDisplayDayCount()).isEqualTo(INSERT_NEW_DISPLAY_COUNT);
+ assertThat(entity.getEditorUseAt()).isEqualTo(INSERT_EDITOR_USE_AT);
+ assertThat(entity.getUploadUseAt()).isEqualTo(INSERT_UPLOAD_USE_AT);
+ assertThat(entity.getUserWriteAt()).isEqualTo(INSERT_USER_WRITE_AT);
+ assertThat(entity.getCommentUseAt()).isEqualTo(INSERT_COMMENT_USE_AT);
+ assertThat(entity.getUploadLimitCount()).isEqualTo(INSERT_UPLOAD_FILE_COUNT);
+ assertThat(entity.getUploadLimitSize()).isEqualTo(INSERT_UPLOAD_LIMIT_SIZE);
+
+ deleteBoard(boardNo);
+ }
+
+ /**
+ * 게시판 수정 테스트
+ */
+ @Test
+ void 게시판_수정() {
+ log.info("###게시판_수정");
+
+ // given
+ Board entity = insertBoard();
+
+ final Integer boardNo = entity.getBoardNo();
+
+ Map params = new HashMap<>();
+ params.put("boardName", UPDATE_BOARD_NAME);
+ params.put("skinTypeCode", UPDATE_SKIN_TYPE_CODE);
+ params.put("titleDisplayLength", UPDATE_TITLE_DISPLAY_LENGTH);
+ params.put("postDisplayCount", UPDATE_POST_DISPLAY_COUNT);
+ params.put("pageDisplayCount", UPDATE_PAGE_DISPLAY_COUNT);
+ params.put("newDisplayDayCount", UPDATE_NEW_DISPLAY_COUNT);
+ params.put("editorUseAt", UPDATE_EDITOR_USE_AT);
+ params.put("uploadUseAt", UPDATE_UPLOAD_USE_AT);
+ params.put("userWriteAt", UPDATE_USER_WRITE_AT);
+ params.put("commentUseAt", UPDATE_COMMENT_USE_AT);
+ params.put("uploadLimitCount", UPDATE_UPLOAD_FILE_COUNT);
+ params.put("uploadLimitSize", UPDATE_UPLOAD_LIMIT_SIZE);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ String url = URL + "/" + boardNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.PUT,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ BoardResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+
+ Optional board = selectData(boardNo);
+ assertThat(board).isPresent();
+
+ Board updatedBoard = board.get();
+ assertThat(updatedBoard.getBoardNo()).isEqualTo(boardNo);
+ assertThat(updatedBoard.getBoardName()).isEqualTo(UPDATE_BOARD_NAME);
+ assertThat(updatedBoard.getSkinTypeCode()).isEqualTo(UPDATE_SKIN_TYPE_CODE);
+ assertThat(updatedBoard.getTitleDisplayLength()).isEqualTo(UPDATE_TITLE_DISPLAY_LENGTH);
+ assertThat(updatedBoard.getPostDisplayCount()).isEqualTo(UPDATE_POST_DISPLAY_COUNT);
+ assertThat(updatedBoard.getPageDisplayCount()).isEqualTo(UPDATE_PAGE_DISPLAY_COUNT);
+ assertThat(updatedBoard.getNewDisplayDayCount()).isEqualTo(UPDATE_NEW_DISPLAY_COUNT);
+ assertThat(updatedBoard.getEditorUseAt()).isEqualTo(UPDATE_EDITOR_USE_AT);
+ assertThat(updatedBoard.getUploadUseAt()).isEqualTo(UPDATE_UPLOAD_USE_AT);
+ assertThat(updatedBoard.getUserWriteAt()).isEqualTo(UPDATE_USER_WRITE_AT);
+ assertThat(updatedBoard.getCommentUseAt()).isEqualTo(UPDATE_COMMENT_USE_AT);
+ assertThat(updatedBoard.getUploadLimitCount()).isEqualTo(UPDATE_UPLOAD_FILE_COUNT);
+ assertThat(updatedBoard.getUploadLimitSize()).isEqualTo(UPDATE_UPLOAD_LIMIT_SIZE);
+
+ deleteBoard(boardNo);
+ }
+
+ /**
+ * 게시판 삭제 테스트
+ */
+ @Test
+ void 게시판_삭제() {
+ log.info("###게시판_삭제");
+
+ // given
+ Board entity = insertBoard();
+
+ final Integer boardNo = entity.getBoardNo();
+
+ String url = URL + "/" + boardNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.DELETE,
+ null,
+ BoardResponseDto.class
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ Optional board = selectData(boardNo);
+ assertThat(board).isNotPresent();
+ }
+
+ /**
+ * 테스트 데이터 등록
+ */
+ private void insertBoards() {
+ log.info("###테스트 데이터 등록");
+
+ List list = new ArrayList<>();
+ for (int i = 1; i <= GIVEN_DATA_COUNT; i++) {
+ list.add(Board.builder()
+ .boardName(BOARD_NAME_PREFIX + "_" + i)
+ .skinTypeCode(SKIN_TYPE_CODE_PREFIX + "_" + i)
+ .titleDisplayLength(INSERT_TITLE_DISPLAY_LENGTH + i)
+ .postDisplayCount(INSERT_POST_DISPLAY_COUNT + i)
+ .pageDisplayCount(INSERT_PAGE_DISPLAY_COUNT + i)
+ .newDisplayDayCount(INSERT_NEW_DISPLAY_COUNT + i)
+ .editorUseAt(i % 2 == 1)
+ .uploadUseAt(i % 3 == 0)
+ .userWriteAt(i % 3 == 0)
+ .commentUseAt(i % 2 == 0)
+ .uploadLimitCount(INSERT_UPLOAD_FILE_COUNT + i)
+ .uploadLimitSize(INSERT_UPLOAD_LIMIT_SIZE.add(new BigDecimal(String.valueOf(i))))
+ .build());
+ }
+
+ boardRepository.saveAll(list);
+ }
+
+ /**
+ * 테스트 데이터 단건 등록
+ *
+ * @return Board 게시판 엔티티
+ */
+ private Board insertBoard() {
+ log.info("###테스트 데이터 단건 등록");
+
+ return boardRepository.save(Board.builder()
+ .boardName(INSERT_BOARD_NAME)
+ .skinTypeCode(INSERT_SKIN_TYPE_CODE)
+ .titleDisplayLength(INSERT_TITLE_DISPLAY_LENGTH)
+ .postDisplayCount(INSERT_POST_DISPLAY_COUNT)
+ .pageDisplayCount(INSERT_PAGE_DISPLAY_COUNT)
+ .newDisplayDayCount(INSERT_NEW_DISPLAY_COUNT)
+ .editorUseAt(INSERT_EDITOR_USE_AT)
+ .uploadUseAt(INSERT_UPLOAD_USE_AT)
+ .userWriteAt(INSERT_USER_WRITE_AT)
+ .commentUseAt(INSERT_COMMENT_USE_AT)
+ .uploadLimitCount(INSERT_UPLOAD_FILE_COUNT)
+ .uploadLimitSize(INSERT_UPLOAD_LIMIT_SIZE)
+ .build());
+ }
+
+ /**
+ * 테스트 데이터 단건 삭제
+ */
+ private void deleteBoard(Integer boardNo) {
+ log.info("###테스트 데이터 단건 삭제");
+
+ boardRepository.deleteById(boardNo);
+ }
+
+ /**
+ * 테스트 데이터 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @return Optional 게시판 엔티티
+ */
+ private Optional selectData(Integer boardNo) {
+ log.info("###테스트 데이터 단건 조회");
+
+ return boardRepository.findById(boardNo);
+ }
+
+}
diff --git a/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/comment/CommentApiControllerTest.java b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/comment/CommentApiControllerTest.java
new file mode 100644
index 0000000..fd00614
--- /dev/null
+++ b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/comment/CommentApiControllerTest.java
@@ -0,0 +1,585 @@
+package org.egovframe.cloud.boardservice.api.comment;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto;
+import org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.board.BoardRepository;
+import org.egovframe.cloud.boardservice.domain.comment.Comment;
+import org.egovframe.cloud.boardservice.domain.comment.CommentId;
+import org.egovframe.cloud.boardservice.domain.comment.CommentRepository;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+import org.egovframe.cloud.boardservice.domain.posts.PostsRepository;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * org.egovframe.cloud.boardservice.api.posts.CommentApiControllerTest
+ *
+ * 댓글 Rest API 컨트롤러 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/11
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/11 jooho 최초 생성
+ *
+ */
+@Slf4j
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@EnableConfigurationProperties
+@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
+@ActiveProfiles(profiles = "test")
+class CommentApiControllerTest {
+
+ /**
+ * test rest template
+ */
+ @Autowired
+ TestRestTemplate restTemplate;
+
+ /**
+ * 게시판 레파지토리 인터페이스
+ */
+ @Autowired
+ BoardRepository boardRepository;
+
+ /**
+ * 게시물 레파지토리 인터페이스
+ */
+ @Autowired
+ PostsRepository postsRepository;
+
+ /**
+ * 댓글 레파지토리 인터페이스
+ */
+ @Autowired
+ CommentRepository commentRepository;
+
+ /**
+ * 게시판 API 경로
+ */
+ private static final String URL = "/api/v1/comments";
+
+ /**
+ * 테스트 데이터 등록 횟수
+ */
+ private final Integer GIVEN_DATA_COUNT = 10;
+
+ /**
+ * 테스트 데이터
+ */
+ private Board board;
+ private Posts posts;
+
+ private final Integer INSERT_COMMENT_NO = 1;
+ private final String INSERT_COMMENT_CONTENT = "댓글 내용";
+
+ private final String UPDATE_COMMENT_CONTENT = "댓글 내용 수정";
+
+ /**
+ * 테스트 시작 전 수행
+ */
+ @BeforeEach
+ void setUp() {
+ log.info("###setUp");
+
+ // 게시판 등록
+ board = boardRepository.save(Board.builder()
+ .boardName("일반게시판1")
+ .skinTypeCode("normal")
+ .titleDisplayLength(50)
+ .postDisplayCount(50)
+ .pageDisplayCount(50)
+ .newDisplayDayCount(50)
+ .editorUseAt(true)
+ .uploadUseAt(true)
+ .uploadLimitCount(50)
+ .uploadLimitSize(new BigDecimal("52428800"))
+ .userWriteAt(true)
+ .commentUseAt(true)
+ .build());
+
+ // 게시물 등록
+ posts = postsRepository.save(Posts.builder()
+ .board(board)
+ .postsId(PostsId.builder()
+ .boardNo(board.getBoardNo())
+ .postsNo(1)
+ .build())
+ .postsTitle("게시물 1")
+ .postsContent("게시물 내용 1")
+ .postsAnswerContent("게시물 답변 내용 1")
+ .attachmentCode("0000000001")
+ .readCount(0)
+ .noticeAt(false)
+ .deleteAt(0)
+ .build());
+ }
+
+ /**
+ * 테스트 종료 후 수행
+ */
+ @AfterEach
+ void tearDown() {
+ log.info("###tearDown");
+
+ // 댓글 삭제
+ commentRepository.deleteAll();
+
+ // 게시물 삭제
+ postsRepository.deleteAll();
+
+ // 게시판 삭제
+ boardRepository.deleteAll();
+ }
+
+ /**
+ * 게시글의 전체 댓글 목록 조회
+ */
+ @Test
+ void 게시글의_전체_댓글_목록_조회() {
+ log.info("###게시글의_전체_댓글_목록_조회");
+
+ // given
+ insertComments();
+
+ final String url = URL + "/total/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo();
+
+ // when
+ ResponseEntity> responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ Map data = responseEntity.getBody();
+
+ assertThat(data).isNotNull();
+ assertThat(data.get("numberOfElements")).isEqualTo(GIVEN_DATA_COUNT);
+ assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT);
+ assertThat(data.get("totalPages")).isEqualTo(1);
+ }
+
+ /**
+ * 게시글의 전체 미삭제 댓글 목록 조회
+ */
+ @Test
+ void 게시글의_전체_미삭제_댓글_목록_조회() {
+ log.info("###게시글의_전체_미삭제_댓글_목록_조회");
+
+ // given
+ insertComments();
+
+ final String url = URL + "/all/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo();
+
+ // when
+ ResponseEntity> responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ Map data = responseEntity.getBody();
+
+ assertThat(data).isNotNull();
+ assertThat(data.get("numberOfElements")).isEqualTo(GIVEN_DATA_COUNT / 3);
+ assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT / 3);
+ assertThat(data.get("totalPages")).isEqualTo(1);
+ }
+
+ /**
+ * 게시글의 댓글 목록 조회
+ */
+ @Test
+ void 게시글의_댓글_목록_조회() {
+ log.info("###게시글의_댓글_목록_조회");
+
+ // given
+ insertComments();
+
+ final Integer page = 0;
+ final Integer size = 5;
+ final String url = URL + "/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo() + "?page=" + page + "&size=" + size;
+
+ // when
+ ResponseEntity> responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ Map data = responseEntity.getBody();
+
+ assertThat(data).isNotNull();
+ assertThat(data.get("numberOfElements")).isEqualTo(size);
+ assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT);
+ assertThat(data.get("totalPages")).isEqualTo(GIVEN_DATA_COUNT / size);
+ }
+
+ /**
+ * 게시글의 미삭제 댓글 목록 조회
+ */
+ @Test
+ void 게시글의_미삭제_댓글_목록_조회() {
+ log.info("###게시글의_미삭제_댓글_목록_조회");
+
+ // given
+ insertComments();
+
+ final Integer page = 0;
+ final Integer size = 5;
+ final String url = URL + "/list/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo() + "?page=" + page + "&size=" + size;
+
+ // when
+ ResponseEntity> responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ Map data = responseEntity.getBody();
+
+ assertThat(data).isNotNull();
+ assertThat(data.get("numberOfElements")).isEqualTo(GIVEN_DATA_COUNT / 3);
+ assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT / 3);
+ assertThat(data.get("totalPages")).isEqualTo(1);
+ }
+
+ /**
+ * 댓글 등록
+ */
+ @Test
+ void 댓글_등록() {
+ log.info("###댓글_등록");
+
+ // given
+ Map params = new HashMap<>();
+ params.put("boardNo", posts.getPostsId().getBoardNo());
+ params.put("postsNo", posts.getPostsId().getPostsNo());
+ params.put("commentContent", INSERT_COMMENT_CONTENT);
+ params.put("depthSeq", 0);
+ params.put("parentCommentNo", null);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ URL,
+ HttpMethod.POST,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ CommentResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+
+ final Integer boardNo = dto.getBoardNo();
+ final Integer postsNo = dto.getPostsNo();
+ final Integer commentNo = dto.getCommentNo();
+
+ Optional comment = selectData(boardNo, postsNo, commentNo);
+ assertThat(comment).isPresent();
+
+ Comment entity = comment.get();
+ assertThat(entity.getCommentId().getPostsId().getBoardNo()).isEqualTo(boardNo);
+ assertThat(entity.getCommentId().getPostsId().getPostsNo()).isEqualTo(postsNo);
+ assertThat(entity.getCommentId().getCommentNo()).isEqualTo(commentNo);
+ assertThat(entity.getCommentContent()).isEqualTo(INSERT_COMMENT_CONTENT);
+ assertThat(entity.getParentCommentNo()).isNull();
+ assertThat(entity.getDeleteAt()).isZero();
+ }
+
+ /**
+ * 댓글 수정 작성자체크
+ */
+ @Test
+ void 댓글_수정_작성자체크() {
+ log.info("###댓글_수정 작성자체크");
+
+ // given
+ Comment entity = insertComment();
+
+ final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo();
+ final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo();
+ final Integer commentNo = entity.getCommentId().getCommentNo();
+
+ Map params = new HashMap<>();
+ params.put("boardNo", boardNo);
+ params.put("postsNo", postsNo);
+ params.put("commentNo", commentNo);
+ params.put("commentContent", UPDATE_COMMENT_CONTENT);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ String url = URL + "/update";
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.PUT,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 본인글 아닌 경우 예외 발생
+ }
+
+ /**
+ * 댓글 삭제 작성자체크
+ */
+ @Test
+ void 댓글_삭제_작성자체크() {
+ log.info("###댓글_삭제 작성자체크");
+
+ // given
+ Comment entity = insertComment();
+
+ final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo();
+ final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo();
+ final Integer commentNo = entity.getCommentId().getCommentNo();
+
+ String url = URL + "/delete/" + boardNo + "/" + postsNo + "/" + commentNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.DELETE,
+ null,
+ BoardResponseDto.class
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 관리자권한 or 본인글 아닌 경우 예외 발생
+ }
+
+ /**
+ * 댓글 수정
+ */
+ @Test
+ void 댓글_수정() {
+ log.info("###댓글_수정");
+
+ // given
+ Comment entity = insertComment();
+
+ final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo();
+ final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo();
+ final Integer commentNo = entity.getCommentId().getCommentNo();
+
+ Map params = new HashMap<>();
+ params.put("boardNo", boardNo);
+ params.put("postsNo", postsNo);
+ params.put("commentNo", commentNo);
+ params.put("commentContent", UPDATE_COMMENT_CONTENT);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ URL,
+ HttpMethod.PUT,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ CommentResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+
+ Optional comment = selectData(boardNo, postsNo, commentNo);
+ assertThat(comment).isPresent();
+
+ Comment updatedComment = comment.get();
+ assertThat(updatedComment.getCommentId().getPostsId().getBoardNo()).isEqualTo(boardNo);
+ assertThat(updatedComment.getCommentId().getPostsId().getPostsNo()).isEqualTo(postsNo);
+ assertThat(updatedComment.getCommentId().getCommentNo()).isEqualTo(commentNo);
+ assertThat(updatedComment.getCommentContent()).isEqualTo(UPDATE_COMMENT_CONTENT);
+ }
+
+ /**
+ * 댓글 삭제
+ */
+ @Test
+ void 댓글_삭제() {
+ log.info("###댓글_삭제");
+
+ // given
+ Comment entity = insertComment();
+
+ final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo();
+ final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo();
+ final Integer commentNo = entity.getCommentId().getCommentNo();
+
+ String url = URL + "/" + boardNo + "/" + postsNo + "/" + commentNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.DELETE,
+ null,
+ BoardResponseDto.class
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ Optional comment = selectData(boardNo, postsNo, commentNo);
+ assertThat(comment).isPresent();
+
+ Comment deletedComment = comment.get();
+ assertThat(deletedComment.getCommentId().getPostsId().getBoardNo()).isEqualTo(boardNo);
+ assertThat(deletedComment.getCommentId().getPostsId().getPostsNo()).isEqualTo(postsNo);
+ assertThat(deletedComment.getCommentId().getCommentNo()).isEqualTo(commentNo);
+ assertThat(deletedComment.getDeleteAt()).isNotZero();
+ }
+
+ /**
+ * 테스트 데이터 등록
+ */
+ private void insertComments() {
+ log.info("###테스트 데이터 등록");
+
+ // 댓글 등록
+ List list = new ArrayList<>();
+ for (int i = 1; i <= GIVEN_DATA_COUNT; i++) {
+ list.add(Comment.builder()
+ .posts(posts)
+ .commentId(CommentId.builder()
+ .postsId(PostsId.builder()
+ .boardNo(posts.getPostsId().getBoardNo())
+ .postsNo(posts.getPostsId().getPostsNo())
+ .build())
+ .commentNo(i)
+ .build())
+ .commentContent(INSERT_COMMENT_CONTENT + i)
+ .groupNo(i)
+ .depthSeq(0)
+ .sortSeq(i)
+ .deleteAt(i % 3)
+ .build());
+ }
+
+ commentRepository.saveAll(list);
+ }
+
+ /**
+ * 테스트 데이터 단건 등록
+ *
+ * @return Comment 댓글 엔티티
+ */
+ private Comment insertComment() {
+ log.info("###테스트 데이터 단건 등록");
+
+ return commentRepository.save(Comment.builder()
+ .posts(posts)
+ .commentId(CommentId.builder()
+ .postsId(PostsId.builder()
+ .boardNo(posts.getPostsId().getBoardNo())
+ .postsNo(posts.getPostsId().getPostsNo())
+ .build())
+ .commentNo(INSERT_COMMENT_NO)
+ .build())
+ .commentContent(INSERT_COMMENT_CONTENT)
+ .groupNo(INSERT_COMMENT_NO)
+ .depthSeq(0)
+ .sortSeq(INSERT_COMMENT_NO)
+ .deleteAt(0)
+ .build());
+ }
+
+ /**
+ * 테스트 데이터 단건 삭제
+ */
+ /*private void deleteComment(Comment comment) {
+ log.info("###테스트 데이터 단건 삭제");
+
+ commentRepository.deleteById(comment.getCommentId());
+ }*/
+
+ /**
+ * 테스트 데이터 삭제
+ */
+ /*private void deleteComments() {
+ log.info("###테스트 데이터 삭제");
+
+ commentRepository.deleteAll(comments);
+
+ comments.clear();
+ }*/
+
+ /**
+ * 테스트 데이터 단건 조회
+ *
+ * @param boardNo 게시판 번호
+ * @param postsNo 게시물 번호
+ * @param commentNo 댓글 번호
+ * @return Optional 댓글 엔티티
+ */
+ private Optional selectData(Integer boardNo, Integer postsNo, Integer commentNo) {
+ log.info("###테스트 데이터 단건 조회");
+
+ return commentRepository.findById(CommentId.builder()
+ .postsId(PostsId.builder()
+ .boardNo(boardNo)
+ .postsNo(postsNo)
+ .build())
+ .commentNo(commentNo)
+ .build());
+ }
+
+}
\ No newline at end of file
diff --git a/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/posts/PostsApiControllerTest.java b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/posts/PostsApiControllerTest.java
new file mode 100644
index 0000000..a404ede
--- /dev/null
+++ b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/posts/PostsApiControllerTest.java
@@ -0,0 +1,730 @@
+package org.egovframe.cloud.boardservice.api.posts;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+import org.assertj.core.api.Condition;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto;
+import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto;
+import org.egovframe.cloud.boardservice.domain.board.Board;
+import org.egovframe.cloud.boardservice.domain.board.BoardRepository;
+import org.egovframe.cloud.boardservice.domain.posts.Posts;
+import org.egovframe.cloud.boardservice.domain.posts.PostsId;
+import org.egovframe.cloud.boardservice.domain.posts.PostsRepository;
+import org.egovframe.cloud.boardservice.util.RestResponsePage;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * org.egovframe.cloud.boardservice.api.posts.PostsApiControllerTest
+ *
+ * 게시물 Rest API 컨트롤러 클래스
+ *
+ * @author 표준프레임워크센터 jooho
+ * @version 1.0
+ * @since 2021/08/10
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2021/08/10 jooho 최초 생성
+ *
+ */
+@Slf4j
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@EnableConfigurationProperties
+@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
+@ActiveProfiles(profiles = "test")
+class PostsApiControllerTest {
+
+ /**
+ * test rest template
+ */
+ @Autowired
+ TestRestTemplate restTemplate;
+
+ /**
+ * 게시판 레파지토리 인터페이스
+ */
+ @Autowired
+ BoardRepository boardRepository;
+
+ /**
+ * 게시물 레파지토리 인터페이스
+ */
+ @Autowired
+ PostsRepository postsRepository;
+
+ /**
+ * 게시물 API 경로
+ */
+ private static final String URL = "/api/v1/posts";
+
+ /**
+ * 테스트 데이터 등록 횟수
+ */
+ private final Integer GIVEN_DATA_COUNT = 10;
+
+ /**
+ * 테스트 데이터
+ */
+ private Board board;
+ private List posts = new ArrayList<>();
+
+ private final Integer INSERT_POSTS_NO = 1;
+ private final String INSERT_POSTS_TITLE = "게시물 제목";
+ private final String INSERT_POSTS_CONTENT = "게시물 내용";
+ private final String INSERT_POSTS_ANSWER_CONTENT = "게시물 답변 내용";
+ private final String INSERT_ATTACHMENT_CODE = "0000000001";
+ private final Integer INSERT_READ_COUNT = 0;
+ private final Boolean INSERT_NOTICE_AT = true;
+
+ private final String UPDATE_POSTS_TITLE = "게시물 제목 수정";
+ private final String UPDATE_POSTS_CONTENT = "게시물 내용 수정";
+ private final String UPDATE_POSTS_ANSWER_CONTENT = "게시물 답변 내용 수정";
+ private final String UPDATE_ATTACHMENT_CODE = "0000000002";
+ private final Boolean UPDATE_NOTICE_AT = false;
+
+ /**
+ * 테스트 시작 전 수행
+ */
+ @BeforeEach
+ void setUp() {
+ log.info("###setUp");
+
+ // 게시판 등록
+ board = boardRepository.save(Board.builder()
+ .boardName("일반게시판1")
+ .skinTypeCode("normal")
+ .titleDisplayLength(50)
+ .postDisplayCount(50)
+ .pageDisplayCount(50)
+ .newDisplayDayCount(50)
+ .editorUseAt(true)
+ .uploadUseAt(true)
+ .uploadLimitCount(50)
+ .uploadLimitSize(new BigDecimal("52428800"))
+ .userWriteAt(false)
+ .commentUseAt(true)
+ .build());
+ }
+
+ /**
+ * 테스트 종료 후 수행
+ */
+ @AfterEach
+ void tearDown() {
+ log.info("###tearDown");
+
+ postsRepository.deleteAll();
+
+ // 게시판 삭제
+ boardRepository.deleteAll();
+ }
+
+ /**
+ * 최근 게시물이 포함된 게시판 목록 조회
+ * @throws JsonProcessingException json exception
+ * @throws JsonMappingException json exception
+ */
+ @Test
+ void 최근_게시물이_포함된_게시판_목록_조회() throws JsonMappingException, JsonProcessingException {
+ log.info("###최근_게시물이_포함된_게시판_목록_조회");
+
+ // given
+ insertPosts(null);
+
+ final Integer postsCount = 3;
+ final String url = URL + "/newest/" + board.getBoardNo() + "/" + postsCount;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode data = mapper.readTree(responseEntity.getBody());
+ JsonNode boardData = data.get(board.getBoardNo().toString());
+
+ assertThat(data).isNotNull();
+ assertThat(boardData.get("boardNo").asInt()).isEqualTo(board.getBoardNo());
+ assertThat(boardData.get("posts").isArray()).isTrue();
+ // assertThat(boardData.get("posts").size()).isEqualTo(postsCount); // h2에서 rownum을 계산하지 못해서 3건, 5건 조회될때가 있음.
+ }
+
+ /**
+ * 게시물 삭제포함 페이지 목록 조회
+ */
+ @Test
+ void 게시물_삭제포함_페이지_목록_조회() {
+ log.info("###게시물_삭제포함_페이지_목록_조회");
+
+ // given
+ insertPosts(null);
+
+ String url = URL + "/" + board.getBoardNo();
+ String queryString = "?keywordType=postsTitle&keyword=" + INSERT_POSTS_TITLE; // 검색 조건
+ queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보
+
+ // when
+ ResponseEntity> responseEntity = restTemplate.exchange(
+ url + queryString,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ RestResponsePage page = responseEntity.getBody();
+ assertThat(page).isNotNull();
+ assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT);
+ assertThat(page.getContent())
+ .isNotEmpty()
+ .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "9").equals(l.get(0).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "9"))
+ .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "6").equals(l.get(1).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "6"));
+ }
+
+ /**
+ * 게시물 삭제제외 페이지 목록 조회
+ */
+ @Test
+ void 게시물_삭제제외_페이지_목록_조회() {
+ log.info("###게시물_삭제제외_페이지_목록_조회");
+
+ // given
+ insertPosts(null);
+
+ String url = URL + "/list/" + board.getBoardNo();
+ String queryString = "?keywordType=postsTitle&keyword=" + INSERT_POSTS_TITLE; // 검색 조건
+ queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보
+
+ // when
+ ResponseEntity> responseEntity = restTemplate.exchange(
+ url + queryString,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ RestResponsePage page = responseEntity.getBody();
+ assertThat(page).isNotNull();
+ assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT / 2);
+ assertThat(page.getContent())
+ .isNotEmpty()
+ .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "6").equals(l.get(0).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "6"))
+ .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "10").equals(l.get(1).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "10"));
+ }
+
+ /**
+ * 게시물 삭제포함 단건 조회
+ */
+ @Test
+ void 게시물_삭제포함_단건_조회() {
+ log.info("###게시물_삭제포함_단건_조회");
+
+ // given
+ final Posts post = insertPost(1);
+
+ String url = URL + "/" + post.getPostsId().getBoardNo() + "/" + post.getPostsId().getPostsNo();
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ PostsResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+ assertThat(dto.getBoardNo()).isEqualTo(post.getPostsId().getBoardNo());
+ assertThat(dto.getPostsNo()).isEqualTo(post.getPostsId().getPostsNo());
+ assertThat(dto.getPostsTitle()).isEqualTo(post.getPostsTitle());
+ assertThat(dto.getPostsContent()).isEqualTo(post.getPostsContent());
+ assertThat(dto.getPostsAnswerContent()).isEqualTo(post.getPostsAnswerContent());
+ assertThat(dto.getAttachmentCode()).isEqualTo(post.getAttachmentCode());
+ assertThat(dto.getReadCount()).isEqualTo(post.getReadCount() + 1);
+ assertThat(dto.getNoticeAt()).isEqualTo(post.getNoticeAt());
+ assertThat(dto.getDeleteAt()).isEqualTo(post.getDeleteAt());
+ }
+
+ /**
+ * 게시물 삭제제외 단건 조회
+ */
+ @Test
+ void 게시물_삭제제외_단건_조회() {
+ log.info("###게시물_삭제제외_단건_조회");
+
+ // given
+ final Posts post = insertPost(1);
+
+ String url = URL + "/view/" + post.getPostsId().getBoardNo() + "/" + post.getPostsId().getPostsNo();
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 삭제 게시물 조회 불가
+ }
+
+ /**
+ * 게시물 등록 작성자체크
+ */
+ @Test
+ void 게시물_등록_작성자체크() {
+ log.info("###게시물_등록_작성자체크");
+
+ // given
+ String url = URL + "/save/" + board.getBoardNo();
+
+ Map params = new HashMap<>();
+ params.put("postsTitle", INSERT_POSTS_TITLE);
+ params.put("postsContent", INSERT_POSTS_CONTENT);
+ params.put("postsAnswerContent", INSERT_POSTS_ANSWER_CONTENT);
+ params.put("attachmentCode", INSERT_ATTACHMENT_CODE);
+ params.put("noticeAt", INSERT_NOTICE_AT);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.POST,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 게시판에 사용자 글쓰기 허용 X
+ }
+
+ /**
+ * 게시물 수정 작성자체크
+ */
+ @Test
+ void 게시물_수정_작성자체크() {
+ log.info("###게시물_수정 작성자체크");
+
+ // given
+ Posts entity = insertPost(0);
+
+ final Integer boardNo = entity.getPostsId().getBoardNo();
+ final Integer postsNo = entity.getPostsId().getPostsNo();
+
+ Map params = new HashMap<>();
+ params.put("postsTitle", UPDATE_POSTS_TITLE);
+ params.put("postsContent", UPDATE_POSTS_CONTENT);
+ params.put("postsAnswerContent", UPDATE_POSTS_ANSWER_CONTENT);
+ params.put("attachmentCode", UPDATE_ATTACHMENT_CODE);
+ params.put("noticeAt", UPDATE_NOTICE_AT);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ String url = URL + "/update/" + boardNo + "/" + postsNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.PUT,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 본인글 아닌 경우 예외 발생
+ }
+
+ /**
+ * 게시물 삭제 작성자체크
+ */
+ @Test
+ void 게시물_삭제_작성자체크() {
+ log.info("###게시물_삭제 작성자체크");
+
+ // given
+ Posts entity = insertPost(0);
+
+ final Integer boardNo = entity.getPostsId().getBoardNo();
+ final Integer postsNo = entity.getPostsId().getPostsNo();
+
+ String url = URL + "/remove/" + boardNo + "/" + postsNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.DELETE,
+ null,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 본인글 아닌 경우 예외 발생
+ }
+
+ /**
+ * 게시물 등록
+ */
+ @Test
+ void 게시물_등록() {
+ log.info("###게시물_등록");
+
+ // given
+ String url = URL + "/" + board.getBoardNo();
+
+ Map params = new HashMap<>();
+ params.put("postsTitle", INSERT_POSTS_TITLE);
+ params.put("postsContent", INSERT_POSTS_CONTENT);
+ params.put("postsAnswerContent", INSERT_POSTS_ANSWER_CONTENT);
+ params.put("attachmentCode", INSERT_ATTACHMENT_CODE);
+ params.put("noticeAt", INSERT_NOTICE_AT);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.POST,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ PostsResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+
+ final Integer boardNo = dto.getBoardNo();
+ final Integer postsNo = dto.getPostsNo();
+
+ Optional posts = selectData(boardNo, postsNo);
+ assertThat(posts).isPresent();
+
+ Posts entity = posts.get();
+ assertThat(entity.getPostsId().getBoardNo()).isEqualTo(boardNo);
+ assertThat(entity.getPostsId().getPostsNo()).isEqualTo(postsNo);
+ assertThat(entity.getPostsTitle()).isEqualTo(INSERT_POSTS_TITLE);
+ assertThat(entity.getPostsContent()).isEqualTo(INSERT_POSTS_CONTENT);
+ assertThat(entity.getPostsAnswerContent()).isEqualTo(INSERT_POSTS_ANSWER_CONTENT);
+ assertThat(entity.getAttachmentCode()).isEqualTo(INSERT_ATTACHMENT_CODE);
+ assertThat(entity.getReadCount()).isZero();
+ assertThat(entity.getNoticeAt()).isEqualTo(INSERT_NOTICE_AT);
+ assertThat(entity.getDeleteAt()).isZero();
+ }
+
+ /**
+ * 게시물 수정
+ */
+ @Test
+ void 게시물_수정() {
+ log.info("###게시물_수정");
+
+ // given
+ Posts entity = insertPost(0);
+
+ final Integer boardNo = entity.getPostsId().getBoardNo();
+ final Integer postsNo = entity.getPostsId().getPostsNo();
+
+ Map params = new HashMap<>();
+ params.put("postsTitle", UPDATE_POSTS_TITLE);
+ params.put("postsContent", UPDATE_POSTS_CONTENT);
+ params.put("postsAnswerContent", UPDATE_POSTS_ANSWER_CONTENT);
+ params.put("attachmentCode", UPDATE_ATTACHMENT_CODE);
+ params.put("noticeAt", UPDATE_NOTICE_AT);
+ HttpEntity> httpEntity = new HttpEntity<>(params);
+
+ String url = URL + "/" + boardNo + "/" + postsNo;
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.PUT,
+ httpEntity,
+ new ParameterizedTypeReference() {
+ }
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ PostsResponseDto dto = responseEntity.getBody();
+ assertThat(dto).isNotNull();
+
+ Optional posts = selectData(boardNo, postsNo);
+ assertThat(posts).isPresent();
+
+ Posts updatedPosts = posts.get();
+ assertThat(updatedPosts.getPostsId().getBoardNo()).isEqualTo(boardNo);
+ assertThat(updatedPosts.getPostsId().getPostsNo()).isEqualTo(postsNo);
+ assertThat(updatedPosts.getPostsTitle()).isEqualTo(UPDATE_POSTS_TITLE);
+ assertThat(updatedPosts.getPostsContent()).isEqualTo(UPDATE_POSTS_CONTENT);
+ assertThat(updatedPosts.getPostsAnswerContent()).isEqualTo(UPDATE_POSTS_ANSWER_CONTENT);
+ assertThat(updatedPosts.getAttachmentCode()).isEqualTo(UPDATE_ATTACHMENT_CODE);
+ assertThat(updatedPosts.getNoticeAt()).isEqualTo(UPDATE_NOTICE_AT);
+ }
+
+ /**
+ * 게시물 다건 삭제
+ */
+ @Test
+ void 게시물_다건_삭제() {
+ log.info("###게시물_다건_삭제");
+
+ // given
+ insertPosts(false);
+
+ List> params = new ArrayList<>();
+ for (Posts post : posts) {
+ Map param = new HashMap<>();
+ param.put("boardNo", post.getPostsId().getBoardNo());
+ param.put("postsNo", post.getPostsId().getPostsNo());
+
+ params.add(param);
+ }
+
+ HttpEntity>> httpEntity = new HttpEntity<>(params);
+
+ String url = URL + "/remove";
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.PUT,
+ httpEntity,
+ Long.class
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(responseEntity.getBody()).isEqualTo(posts.size());
+
+ List list = postsRepository.findAll();
+
+ assertThat(list).isNotNull();
+ assertThat(list.size()).isEqualTo(posts.size());
+
+ for (Posts post : list) {
+ assertThat(post).isNotNull();
+ assertThat(post.getDeleteAt()).isNotZero();
+ }
+ }
+
+ /**
+ * 게시물 다건 복원
+ */
+ @Test
+ void 게시물_다건_복원() {
+ log.info("###게시물_다건_복원");
+
+ // given
+ insertPosts(true);
+
+ List> params = new ArrayList<>();
+ for (Posts post : posts) {
+ Map param = new HashMap<>();
+ param.put("boardNo", post.getPostsId().getBoardNo());
+ param.put("postsNo", post.getPostsId().getPostsNo());
+
+ params.add(param);
+ }
+
+ HttpEntity>> httpEntity = new HttpEntity<>(params);
+
+ String url = URL + "/restore";
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.PUT,
+ httpEntity,
+ Long.class
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(responseEntity.getBody()).isEqualTo(posts.size());
+
+ List list = postsRepository.findAll();
+
+ assertThat(list).isNotNull();
+ assertThat(list.size()).isEqualTo(posts.size());
+
+ for (Posts post : list) {
+ assertThat(post).isNotNull();
+ assertThat(post.getDeleteAt()).isZero();
+ }
+ }
+
+ /**
+ * 게시물 다건 완전 삭제
+ */
+ @Test
+ void 게시물_다건_완전_삭제() {
+ log.info("###게시물_다건_완전_삭제");
+
+ // given
+ insertPosts(null);
+
+ List> params = new ArrayList<>();
+ for (Posts post : posts) {
+ Map param = new HashMap<>();
+ param.put("boardNo", post.getPostsId().getBoardNo());
+ param.put("postsNo", post.getPostsId().getPostsNo());
+
+ params.add(param);
+ }
+
+ HttpEntity>> httpEntity = new HttpEntity<>(params);
+
+ String url = URL + "/delete";
+
+ // when
+ ResponseEntity responseEntity = restTemplate.exchange(
+ url,
+ HttpMethod.PUT,
+ httpEntity,
+ Long.class
+ );
+
+ // then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ List list = postsRepository.findAll();
+
+ assertThat(list).isNotNull();
+ assertThat(list.size()).isZero();
+ }
+
+ /**
+ * 테스트 데이터 등록
+ */
+ private void insertPosts(Boolean deleteAt) {
+ log.info("###테스트 데이터 등록");
+
+ // 게시물 등록
+ List