Initial commit

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

View File

@@ -0,0 +1,33 @@
package org.egovframe.cloud.reserverequestservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import reactor.blockhound.BlockHound;
import java.security.Security;
@ComponentScan({"org.egovframe.cloud.common", "org.egovframe.cloud.reactive", "org.egovframe.cloud.reserverequestservice"}) // org.egovframe.cloud.common package 포함하기 위해
@EnableDiscoveryClient
@SpringBootApplication
public class ReserveRequestServiceApplication {
public static void main(String[] args) {
// TLSv1/v1.1 No longer works after upgrade, "No appropriate protocol" error
String property = Security.getProperty("jdk.tls.disabledAlgorithms").replace(", TLSv1", "").replace(", TLSv1.1", "");
Security.setProperty("jdk.tls.disabledAlgorithms", property);
//blocking 코드 감지
BlockHound.builder()
//mysql r2dbc 에서 호출되는 FileInputStream.readBytes() 가 블로킹코드인데 이를 허용해주도록 한다.
//해당 코드가 어디서 호출되는지 알지 못하는 상태에서 FileInputStream.readBytes() 자체를 허용해주는 것은 좋지 않다.
// 누군가 무분별하게 사용하게 되면 검출해 낼 수ㅂ 없어 시스템의 위험요소로 남게 된다.
// r2dbc를 사용하기 위해 해당 호출부분만 허용하고 나머지는 여전히 검출대상으로 남기도록 한다.
.allowBlockingCallsInside("dev.miku.r2dbc.mysql.client.ReactorNettyClient", "init")
.install();
SpringApplication.run(ReserveRequestServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,124 @@
package org.egovframe.cloud.reserverequestservice.api;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.reserverequestservice.api.dto.ReserveResponseDto;
import org.egovframe.cloud.reserverequestservice.api.dto.ReserveSaveRequestDto;
import org.egovframe.cloud.reserverequestservice.config.MessageListenerContainerFactory;
import org.egovframe.cloud.reserverequestservice.domain.Category;
import org.egovframe.cloud.reserverequestservice.service.ReserveService;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* org.egovframe.cloud.reserverequestservice.api.ReserveApiController
*
* 예약 신청 rest controller class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/16 shinmj 최초 생성
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@RestController
public class ReserveApiController {
private final ReserveService reserveService;
private final MessageListenerContainerFactory messageListenerContainerFactory;
private final AmqpAdmin amqpAdmin;
/**
* 예약 신청 - 심사
*
* @param saveRequestDtoMono
* @return
*/
@PostMapping("/api/v1/requests/audit")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ReserveResponseDto> create(@RequestBody Mono<ReserveSaveRequestDto> saveRequestDtoMono) {
return saveRequestDtoMono.flatMap(reserveService::create);
}
/**
* 예약 신청 - 실시간
*
* @param saveRequestDtoMono
* @return
*/
@PostMapping("/api/v1/requests")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ReserveResponseDto> save(@RequestBody Mono<ReserveSaveRequestDto> saveRequestDtoMono) {
return saveRequestDtoMono.flatMap(saveRequestDto -> {
if (Category.EDUCATION.isEquals(saveRequestDto.getCategoryId())) {
return reserveService.saveForEvent(saveRequestDto);
}
return reserveService.save(saveRequestDto);
});
}
/**
* 실시간 예약 신청 후 결과 여부 subscribe
*
* @param reserveId
* @return
*/
@GetMapping(value = "/api/v1/requests/direct/{reserveId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<?> receiveReservationResult(@PathVariable String reserveId) {
MessageListenerContainer mlc = messageListenerContainerFactory.createMessageListenerContainer(reserveId);
Flux<String> f = Flux.create(emitter -> {
mlc.setupMessageListener((MessageListener) m -> {
String qname = m.getMessageProperties().getConsumerQueue();
log.info("message received, queue={}", qname);
if (emitter.isCancelled()) {
log.info("cancelled, queue={}", qname);
mlc.stop();
return;
}
String payload = new String(m.getBody());
log.info("message data = {}", payload);
emitter.next(payload);
log.info("message sent to client, queue={}", qname);
});
emitter.onRequest(v -> {
log.info("starting container, queue={}", reserveId);
mlc.start();
});
emitter.onDispose(() -> {
log.info("on dispose, queue={}", reserveId);
mlc.stop();
amqpAdmin.deleteQueue(reserveId);
});
log.info("container started, queue={}", reserveId);
});
return Flux.interval(Duration.ofSeconds(5))
.map(v -> {
log.info("sending keepalive message...");
return "no news is good news";
}).mergeWith(f);
}
}

View File

@@ -0,0 +1,64 @@
package org.egovframe.cloud.reserverequestservice.api.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.egovframe.cloud.reserverequestservice.domain.Reserve;
import java.time.LocalDateTime;
/**
* org.egovframe.cloud.reserverequestservice.api.dto.ReserveResponseDto
* <p>
* 예약 신청 응답 dto class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/17
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/17 shinmj 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor
@ToString
public class ReserveResponseDto {
private String reserveId;
private Long reserveItemId;
private Long locationId;
private String categoryId;
private Integer reserveQty;
private LocalDateTime reserveStartDate;
private LocalDateTime reserveEndDate;
private String reservePurposeContent;
private String attachmentCode;
private String userId;
private String userContactNo;
private String userEmail;
@Builder
public ReserveResponseDto(Reserve entity) {
this.reserveId = entity.getReserveId();
this.reserveItemId = entity.getReserveItemId();
this.locationId = entity.getLocationId();
this.categoryId = entity.getCategoryId();
this.reserveQty = entity.getReserveQty();
this.reserveStartDate = entity.getReserveStartDate();
this.reserveEndDate = entity.getReserveEndDate();
this.reservePurposeContent = entity.getReservePurposeContent();
this.attachmentCode = entity.getAttachmentCode();
this.userId = entity.getUserId();
this.userContactNo = entity.getUserContactNo();
this.userEmail = entity.getUserEmail();
}
}

View File

@@ -0,0 +1,111 @@
package org.egovframe.cloud.reserverequestservice.api.dto;
import lombok.*;
import org.egovframe.cloud.reserverequestservice.domain.Reserve;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* org.egovframe.cloud.reserverequestservice.api.dto.ReserveSaveRequestDto
* <p>
* 예약 신청 저장 요청 dto class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/17
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/17 shinmj 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor
@ToString
public class ReserveSaveRequestDto {
@Setter
private String reserveId;
@NotNull
private Long reserveItemId;
private Long locationId;
private String categoryId;
private Integer totalQty;
private String reserveMethodId;
private String reserveMeansId;
private LocalDateTime operationStartDate;
private LocalDateTime operationEndDate;
private LocalDateTime requestStartDate;
private LocalDateTime requestEndDate;
private Boolean isPeriod;
private Integer periodMaxCount;
private Integer reserveQty; //예약 신청 인원/수량
@NotNull
private String reservePurposeContent; //예약 목적
private String attachmentCode; //첨부파일 코드
private LocalDateTime reserveStartDate; //예약 신청 시작일
private LocalDateTime reserveEndDate; //예약 신청 종료일
@Setter
private String reserveStatusId; //예약상태 - 공통코드(reserve-status)
@NotNull
private String userId; //예약자
@NotNull
private String userContactNo; //예약자 연락처
@NotNull
private String userEmail; //예약자 이메일
@Builder
public ReserveSaveRequestDto(String reserveId, Long reserveItemId, Long locationId, String categoryId,
Integer totalQty, String reserveMethodId, String reserveMeansId, LocalDateTime operationStartDate,
LocalDateTime operationEndDate, LocalDateTime requestStartDate, LocalDateTime requestEndDate,
Boolean isPeriod, Integer periodMaxCount, Integer reserveQty, String reservePurposeContent,
String attachmentCode, LocalDateTime reserveStartDate, LocalDateTime reserveEndDate,
String reserveStatusId, String userId, String userContactNo, String userEmail) {
this.reserveId = reserveId;
this.reserveItemId = reserveItemId;
this.locationId = locationId;
this.categoryId = categoryId;
this.totalQty = totalQty;
this.reserveMethodId = reserveMethodId;
this.reserveMeansId = reserveMeansId;
this.operationStartDate = operationStartDate;
this.operationEndDate = operationEndDate;
this.requestStartDate = requestStartDate;
this.requestEndDate = requestEndDate;
this.isPeriod = isPeriod;
this.periodMaxCount = periodMaxCount;
this.reserveQty = reserveQty;
this.reservePurposeContent = reservePurposeContent;
this.attachmentCode = attachmentCode;
this.reserveStartDate = reserveStartDate;
this.reserveEndDate = reserveEndDate;
this.reserveStatusId = reserveStatusId;
this.userId = userId;
this.userContactNo = userContactNo;
this.userEmail = userEmail;
}
public Reserve toEntity() {
return Reserve.builder()
.reserveId(this.reserveId)
.reserveItemId(this.reserveItemId)
.locationId(this.locationId)
.categoryId(this.categoryId)
.reserveQty(this.reserveQty)
.reservePurposeContent(this.reservePurposeContent)
.attachmentCode(this.attachmentCode)
.reserveStartDate(this.reserveStartDate)
.reserveEndDate(this.reserveEndDate)
.reserveStatusId(this.reserveStatusId)
.userId(this.userId)
.userContactNo(this.userContactNo)
.userEmail(this.userEmail)
.build();
}
}

View File

@@ -0,0 +1,44 @@
package org.egovframe.cloud.reserverequestservice.config;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* org.egovframe.cloud.reserverequestservice.config.MessageListenerContainerFactory
*
* 동적으로 이벤트 큐 생성하기 위한 component
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/30 shinmj 최초 생성
* </pre>
*/
@NoArgsConstructor
@Component
public class MessageListenerContainerFactory {
@Autowired
private ConnectionFactory connectionFactory;
public MessageListenerContainer createMessageListenerContainer(String queueName) {
SimpleMessageListenerContainer mlc = new SimpleMessageListenerContainer();
mlc.setConnectionFactory(connectionFactory);
mlc.addQueueNames(queueName);
mlc.setAcknowledgeMode(AcknowledgeMode.AUTO);
return mlc;
}
}

View File

@@ -0,0 +1,37 @@
package org.egovframe.cloud.reserverequestservice.config;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* org.egovframe.cloud.reserverequestservice.config.RequestMessage
*
* 예약 신청 후 이벤트 스트림 message VO class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/16 shinmj 최초 생성
* </pre>
*/
@NoArgsConstructor
@Getter
@ToString
public class RequestMessage {
private String reserveId;
private Boolean isItemUpdated;
@Builder
public RequestMessage(String reserveId, Boolean isItemUpdated) {
this.reserveId = reserveId;
this.isItemUpdated = isItemUpdated;
}
}

View File

@@ -0,0 +1,75 @@
package org.egovframe.cloud.reserverequestservice.config;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.common.config.GlobalConstant;
import org.egovframe.cloud.reserverequestservice.domain.ReserveStatus;
import org.egovframe.cloud.reserverequestservice.service.ReserveService;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import java.util.function.Consumer;
/**
* org.egovframe.cloud.reserverequestservice.config.ReserveEventConfig
*
* event stream 설정 class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/16 shinmj 최초 생성
* </pre>
*/
@Slf4j
@Configuration
public class ReserveEventConfig {
@Autowired
private ReserveService reserveService;
@Autowired
private ConnectionFactory connectionFactory;
/**
* 예약 신청(실시간) 후 재고 변경에 대한 성공 여부 consumer function
*
* @return
*/
@Bean
public Consumer<Message<RequestMessage>> inventoryUpdated() {
return message -> {
log.info("receive message: {}, headers: {}", message.getPayload(), message.getHeaders());
if (message.getPayload().getIsItemUpdated()) {
reserveService.updateStatus(message.getPayload().getReserveId(), ReserveStatus.APPROVE).subscribe();
}else {
reserveService.delete(message.getPayload().getReserveId()).subscribe();
}
RabbitTemplate rabbitTemplate = rabbitTemplate(connectionFactory);
rabbitTemplate.convertAndSend(GlobalConstant.SUCCESS_OR_NOT_EX_NAME,
message.getPayload().getReserveId(), message.getPayload().getIsItemUpdated());
};
}
@Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter());
return rabbitTemplate;
}
@Bean
public Jackson2JsonMessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}

View File

@@ -0,0 +1,19 @@
package org.egovframe.cloud.reserverequestservice.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Category {
EDUCATION("education", "교육"),
EQUIPMENT("equipment", "장비"),
SPACE("space", "공간");
private final String key;
private final String title;
public boolean isEquals(String compare) {
return this.getKey().equals(compare);
}
}

View File

@@ -0,0 +1,119 @@
package org.egovframe.cloud.reserverequestservice.domain;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.egovframe.cloud.reactive.domain.BaseEntity;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
/**
* org.egovframe.cloud.reserverequestservice.domain.Reserve
*
* 예약 도메인 클래스
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/15
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/15 shinmj 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor
@ToString
@Table("reserve")
public class Reserve extends BaseEntity {
@Id
@Column
private String reserveId; //예약 id
@Column
private Long reserveItemId; //예약 물품 id
@Column
private Long locationId; //예약 물품 - 지역 id
@Column
private String categoryId; //예약 물품 - 유형 id
@Column
private Integer reserveQty; //예약 신청 인원/수량
@Column
private String reservePurposeContent; //예약 목적
@Column
private String attachmentCode; //첨부파일 코드
@Column
private LocalDateTime reserveStartDate; //예약 신청 시작일
@Column
private LocalDateTime reserveEndDate; //예약 신청 종료일
@Column
private String reserveStatusId; //예약상태 - 공통코드(reserve-status)
@Column
private String userId; //예약자
@Column
private String userContactNo; //예약자 연락처
@Column("user_email_addr")
private String userEmail; //예약자 이메일
@Builder
public Reserve(String reserveId, Long reserveItemId, Long locationId, String categoryId, Integer reserveQty, String reservePurposeContent, String attachmentCode, LocalDateTime reserveStartDate, LocalDateTime reserveEndDate, String reserveStatusId, String userId, String userContactNo, String userEmail) {
this.reserveId = reserveId;
this.reserveItemId = reserveItemId;
this.locationId = locationId;
this.categoryId = categoryId;
this.reserveQty = reserveQty;
this.reservePurposeContent = reservePurposeContent;
this.attachmentCode = attachmentCode;
this.reserveStartDate = reserveStartDate;
this.reserveEndDate = reserveEndDate;
this.reserveStatusId = reserveStatusId;
this.userId = userId;
this.userContactNo = userContactNo;
this.userEmail = userEmail;
}
/**
* 예약 상태 업데이트
*
* @param reserveStatusId
* @return
*/
public Reserve updateStatus(String reserveStatusId) {
this.reserveStatusId = reserveStatusId;
return this;
}
/**
* create 정보 세팅
* insert 시 필요
*
* @param createdDate
* @param createdBy
* @return
*/
public Reserve setCreatedInfo(LocalDateTime createdDate, String createdBy) {
this.createdBy = createdBy;
this.createDate = createdDate;
return this;
}
}

View File

@@ -0,0 +1,25 @@
package org.egovframe.cloud.reserverequestservice.domain;
import org.egovframe.cloud.reserverequestservice.domain.Reserve;
import org.egovframe.cloud.reserverequestservice.domain.ReserveRepositoryCustom;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
/**
* org.egovframe.cloud.reserverequestservice.domain.Reserve
*
* 예약 도메인 repository interface
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/15
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/15 shinmj 최초 생성
* </pre>
*/
public interface ReserveRepository extends R2dbcRepository<Reserve, String>, ReserveRepositoryCustom {
}

View File

@@ -0,0 +1,28 @@
package org.egovframe.cloud.reserverequestservice.domain;
import java.time.LocalDateTime;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* org.egovframe.cloud.reserverequestservice.domain.ReserveRepositoryCustom
*
* 예약 도메인 repository custom interface
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/27
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/27 shinmj 최초 생성
* </pre>
*/
public interface ReserveRepositoryCustom {
Mono<Reserve> insert(Reserve reserve);
Flux<Reserve> findAllByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate);
Mono<Long> findAllByReserveDateWithoutSelfCount(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate);
}

View File

@@ -0,0 +1,86 @@
package org.egovframe.cloud.reserverequestservice.domain;
import static org.springframework.data.relational.core.query.Criteria.*;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* org.egovframe.cloud.reserverequestservice.domain.ReserveRepositoryImpl
*
* 예약 도메인 repository custom interface 구현체
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/27
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/27 shinmj 최초 생성
* </pre>
*/
@RequiredArgsConstructor
public class ReserveRepositoryImpl implements ReserveRepositoryCustom {
private final R2dbcEntityTemplate entityTemplate;
/**
* 예약 insert
* pk(reserveId)를 서비스에서 생성하여 insert 하기 위함.
*
* @param reserve
* @return
*/
@Override
public Mono<Reserve> insert(Reserve reserve) {
return entityTemplate.insert(reserve);
}
/**
* 조회 기간에 예약된 건 조회
* 현 예약건은 제외
*
* @param reserveItemId
* @param startDate
* @param endDate
* @return
*/
@Override
public Flux<Reserve> findAllByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) {
return entityTemplate.select(Reserve.class)
.matching(Query.query(where("reserve_item_id").is(reserveItemId)
.and ("reserve_start_date").lessThanOrEquals(endDate)
.and("reserve_end_date").greaterThanOrEquals(startDate)
.and("reserve_id").not(reserveId)
))
.all();
}
/**
* 조회 기간에 예약된 건수 조회
* 현 예약건은 제외
*
* @param reserveItemId
* @param startDate
* @param endDate
* @return
*/
@Override
public Mono<Long> findAllByReserveDateWithoutSelfCount(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) {
return entityTemplate.select(Reserve.class)
.matching(Query.query(where("reserve_item_id").is(reserveItemId)
.and ("reserve_start_date").lessThanOrEquals(endDate)
.and("reserve_end_date").greaterThanOrEquals(startDate)
.and("reserve_id").not(reserveId)
))
.count();
}
}

View File

@@ -0,0 +1,16 @@
package org.egovframe.cloud.reserverequestservice.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum ReserveStatus {
REQUEST("request", "예약 신청"),
APPROVE("approve", "예약 승인"),
CANCEL("cancel", "예약 취소"),
DONE("done", "완료");
private final String key;
private final String title;
}

View File

@@ -0,0 +1,311 @@
package org.egovframe.cloud.reserverequestservice.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.cloud.common.config.GlobalConstant;
import org.egovframe.cloud.common.exception.BusinessMessageException;
import org.egovframe.cloud.reactive.service.ReactiveAbstractService;
import org.egovframe.cloud.reserverequestservice.api.dto.ReserveResponseDto;
import org.egovframe.cloud.reserverequestservice.api.dto.ReserveSaveRequestDto;
import org.egovframe.cloud.reserverequestservice.domain.Category;
import org.egovframe.cloud.reserverequestservice.domain.Reserve;
import org.egovframe.cloud.reserverequestservice.domain.ReserveRepository;
import org.egovframe.cloud.reserverequestservice.domain.ReserveStatus;
import org.springframework.amqp.core.*;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import java.util.stream.IntStream;
/**
* org.egovframe.cloud.reserverequestservice.service.ReserveService
* <p>
* 예약 신청 service class
*
* @author 표준프레임워크센터 shinmj
* @version 1.0
* @since 2021/09/17
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/17 shinmj 최초 생성
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class ReserveService extends ReactiveAbstractService {
private final ReserveRepository reserveRepository;
private final StreamBridge streamBridge;
private final AmqpAdmin amqpAdmin;
/**
* entity -> dto 변환
*
* @param reserve
* @return
*/
private Mono<ReserveResponseDto> convertReserveResponseDto(Reserve reserve) {
return Mono.just(ReserveResponseDto.builder()
.entity(reserve)
.build());
}
/**
* 현재 로그인 사용자 id
*
* @return
*/
private Mono<String> getUserId() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(String.class::cast);
}
/**
* 예약 신청 저장
*
* @param saveRequestDto
* @return
*/
public Mono<ReserveResponseDto> create(ReserveSaveRequestDto saveRequestDto) {
return Mono.just(saveRequestDto)
.flatMap(dto -> {
String uuid = UUID.randomUUID().toString();
dto.setReserveId(uuid);
dto.setReserveStatusId(ReserveStatus.REQUEST.getKey());
return Mono.just(dto.toEntity());
})
.zipWith(getUserId())
.flatMap(tuple -> {
tuple.getT1().setCreatedInfo(LocalDateTime.now(), tuple.getT2());
return Mono.just(tuple.getT1());
})
.flatMap(reserveRepository::insert)
.flatMap(this::convertReserveResponseDto);
}
/**
* 예약 신청 - 실시간
* 예약 정보 저장 후 재고 변경을 위해 이벤트 publish
*
* @param saveRequestDto
* @return
*/
public Mono<ReserveResponseDto> saveForEvent(ReserveSaveRequestDto saveRequestDto) {
return create(saveRequestDto)
.flatMap(reserveResponseDto ->
Mono.fromCallable(() -> {
//예약 저장 후 해당 id로 queue 생성
Exchange ex = ExchangeBuilder.directExchange(GlobalConstant.SUCCESS_OR_NOT_EX_NAME)
.durable(true).build();
amqpAdmin.declareExchange(ex);
Queue queue = QueueBuilder.durable(reserveResponseDto.getReserveId()).build();
amqpAdmin.declareQueue(queue);
Binding binding = BindingBuilder.bind(queue)
.to(ex)
.with(reserveResponseDto.getReserveId())
.noargs();
amqpAdmin.declareBinding(binding);
log.info("Biding successfully created");
streamBridge.send("reserveRequest-out-0", reserveResponseDto);
return reserveResponseDto;
}).subscribeOn(Schedulers.boundedElastic())
);
}
/**
* 예약 신청 - 실시간
* 이벤트 스트림을 타지 않는 경우 (재고 변경 이벤트가 없는 경우: 공간, 장비)
*
* @param saveRequestDto
* @return
*/
public Mono<ReserveResponseDto> save(ReserveSaveRequestDto saveRequestDto) {
return Mono.just(saveRequestDto)
.flatMap(this::checkValidation)
.onErrorResume(throwable -> Mono.error(throwable))
.flatMap(dto -> {
String uuid = UUID.randomUUID().toString();
dto.setReserveId(uuid);
dto.setReserveStatusId(ReserveStatus.APPROVE.getKey());
return Mono.just(dto.toEntity());
}).zipWith(getUserId())
.flatMap(tuple -> Mono.just(tuple.getT1().setCreatedInfo(LocalDateTime.now(), tuple.getT2())))
.flatMap(reserveRepository::insert)
.flatMap(this::convertReserveResponseDto);
}
private Mono<ReserveSaveRequestDto> checkValidation(ReserveSaveRequestDto saveRequestDto) {
if (Category.EQUIPMENT.isEquals(saveRequestDto.getCategoryId())) {
return checkEquipment(saveRequestDto);
}else if (Category.SPACE.isEquals(saveRequestDto.getCategoryId())) {
return checkSpace(saveRequestDto);
}
return Mono.error(new BusinessMessageException("저장 할 수 없습니다."));
}
/**
* 예약 날자 validation
*
* @param saveRequestDto
* @return
*/
private Mono<ReserveSaveRequestDto> checkReserveDate(ReserveSaveRequestDto saveRequestDto) {
LocalDateTime startDate = saveRequestDto.getReserveMeansId().equals("realtime") ?
saveRequestDto.getRequestStartDate() : saveRequestDto.getOperationStartDate();
LocalDateTime endDate = saveRequestDto.getReserveMeansId().equals("realtime") ?
saveRequestDto.getRequestEndDate() : saveRequestDto.getOperationEndDate();
if (saveRequestDto.getReserveStartDate().isBefore(startDate)) {
return Mono.error(new BusinessMessageException("시작일이 운영/예약 시작일 이전입니다."));
}
if (saveRequestDto.getReserveEndDate().isAfter(endDate)) {
return Mono.error(new BusinessMessageException("종료일이 운영/예약 종료일 이후입니다."));
}
if (saveRequestDto.getIsPeriod()) {
long between = ChronoUnit.DAYS.between(saveRequestDto.getReserveStartDate(),
saveRequestDto.getReserveEndDate());
if (saveRequestDto.getPeriodMaxCount() < between) {
return Mono.error(new BusinessMessageException("최대 예약 가능 일수보다 예약기간이 깁니다. (최대 예약 가능일 수 : "+saveRequestDto.getPeriodMaxCount()+")"));
}
}
return Mono.just(saveRequestDto);
}
/**
* 공간 예약 시 예약 날짜에 다른 예약이 있는지 체크
*
* @param saveRequestDto
* @return
*/
private Mono<ReserveSaveRequestDto> checkSpace(ReserveSaveRequestDto saveRequestDto) {
return this.checkReserveDate(saveRequestDto)
.flatMap(result -> reserveRepository.findAllByReserveDateWithoutSelfCount(
result.getReserveId(),
result.getReserveItemId(),
result.getReserveStartDate(),
result.getReserveEndDate())
.flatMap(count -> {
if (count > 0) {
return Mono.error(new BusinessMessageException("해당 날짜에는 예약할 수 없습니다."));
}
return Mono.just(result);
})
);
}
/**
* 장비 예약 시 예약 날짜에 예약 가능한 재고 체크
*
* @param saveRequestDto
* @return
*/
private Mono<ReserveSaveRequestDto> checkEquipment(ReserveSaveRequestDto saveRequestDto) {
return this.checkReserveDate(saveRequestDto)
.flatMap(result -> this.getMaxByReserveDateWithoutSelf(
result.getReserveId(),
result.getReserveItemId(),
result.getReserveStartDate(),
result.getReserveEndDate())
.flatMap(max -> {
if ((result.getTotalQty() - max) < result.getReserveQty()) {
return Mono.just(false);
}
return Mono.just(true);
})
.flatMap(isValid -> {
if (!isValid) {
return Mono.error(new BusinessMessageException("해당 날짜에 예약할 수 있는 재고수량이 없습니다."));
}
return Mono.just(saveRequestDto);
})
);
}
/**
* 예약물품에 대해 날짜별 예약된 수량 max 조회
* 현 예약 건 제외
*
* @param reserveItemId
* @param startDate
* @param endDate
* @return
*/
private Mono<Integer> getMaxByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) {
Flux<Reserve> reserveFlux = reserveRepository.findAllByReserveDateWithoutSelf(reserveId, reserveItemId, startDate, endDate)
.switchIfEmpty(Flux.empty());
if (reserveFlux.equals(Flux.empty())) {
return Mono.just(0);
}
long between = ChronoUnit.DAYS.between(startDate, endDate);
return Flux.fromStream(IntStream.iterate(0, i -> i + 1)
.limit(between)
.mapToObj(i -> startDate.plusDays(i)))
.flatMap(localDateTime ->
reserveFlux.map(findReserve -> {
if (localDateTime.isAfter(findReserve.getReserveStartDate())
|| localDateTime.isBefore(findReserve.getReserveEndDate())) {
return findReserve.getReserveQty();
}
return 0;
}).reduce(0, (x1, x2) -> x1 + x2))
.groupBy(integer -> integer)
.flatMap(group -> group.reduce((x1,x2) -> x1 > x2?x1:x2))
.last();
}
/**
* 예약 신청 후 예약 물품 재고 변경 성공 시 예약승인으로 상태 변경
*
* @param reserveId
* @param reserveStatus
* @return
*/
public Mono<Void> updateStatus(String reserveId, ReserveStatus reserveStatus) {
log.info("update : {} , {}", reserveId, reserveStatus);
return reserveRepository.findById(reserveId)
.map(reserve -> reserve.updateStatus(reserveStatus.getKey()))
.flatMap(reserveRepository::save)
.then();
}
/**
* 예약 신청 후 예약 물품 재고 변경 실패 시 해당 예약 건 삭제
*
* @param reserveId
* @return
*/
public Mono<Void> delete(String reserveId) {
log.info("delete {}", reserveId);
return reserveRepository.findById(reserveId)
.flatMap(reserveRepository::delete)
.then();
}
}

View File

@@ -0,0 +1,13 @@
spring:
application:
name: reserve-request-service
server:
port: 0
# config server actuator
management:
endpoints:
web:
exposure:
include: refresh, health, beans

View File

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

View File

@@ -0,0 +1,26 @@
-- reserve Table Create SQL
CREATE TABLE IF NOT EXISTS reserve
(
`reserve_id` VARCHAR(255) NOT NULL COMMENT '예약 id',
`reserve_item_id` BIGINT NULL COMMENT '예약 물품 id',
`location_id` BIGINT NULL COMMENT '예약 물품-지역 id',
`category_id` VARCHAR(255) NULL COMMENT '예약 물품-유형 id',
`reserve_qty` BIGINT(18) NULL COMMENT '예약 신청인원/수량',
`reserve_purpose_content` VARCHAR(4000) NULL COMMENT '예약신청 목적',
`attachment_code` VARCHAR(255) NULL COMMENT '첨부파일 코드',
`reserve_start_date` DATETIME NULL COMMENT '예약 신청 시작일',
`reserve_end_date` DATETIME NULL COMMENT '예약 신청 종료일',
`reserve_status_id` VARCHAR(20) NULL COMMENT '예약상태 - 공통코드(reserve-status)',
`reason_cancel_content` VARCHAR(4000) NULL COMMENT '예약 취소 사유',
`user_id` VARCHAR(255) NULL COMMENT '예약자 id',
`user_contact_no` VARCHAR(50) NULL COMMENT '예약자 연락처',
`user_email_addr` VARCHAR(500) NULL COMMENT '예약자 이메일',
`create_date` DATETIME NULL COMMENT '생성일',
`created_by` VARCHAR(255) NULL COMMENT '생성자',
`modified_date` DATETIME NULL COMMENT '수정일',
`last_modified_by` VARCHAR(255) NULL COMMENT '수정자',
PRIMARY KEY (reserve_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE reserve COMMENT '예약 신청&확인';