diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml index a5e3fe537..263a22758 100644 --- a/backend/docker-compose.yaml +++ b/backend/docker-compose.yaml @@ -6,11 +6,11 @@ services: build: ./ restart: always environment: - MARIADB_DATABASE_HOST: ${HOST} - MARIADB_DATABASE: ${DATABASE} - MARIADB_USER: ${DB_USER} - MARIADB_PASSWORD: ${DB_PASSWORD} - MARIADB_ROOT_PASSWORD: ${ROOT_PASSWORD} + MARIADB_DATABASE_HOST: localhost + MARIADB_DATABASE: cabi_local + MARIADB_USER: root + MARIADB_PASSWORD: YourPassword + MARIADB_ROOT_PASSWORD: YourPassword ports: - '3307:3306' tty: true diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/UserBlackholeInfoDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/UserBlackholeInfoDto.java index 2ac7e21f3..7e0772a5a 100644 --- a/backend/src/main/java/org/ftclub/cabinet/dto/UserBlackholeInfoDto.java +++ b/backend/src/main/java/org/ftclub/cabinet/dto/UserBlackholeInfoDto.java @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; +import org.ftclub.cabinet.user.domain.User; /** * 유저의 식별자, 이름, 이메일, 블랙홀 날짜를 반환하는 DTO입니다. @@ -22,4 +23,7 @@ public static UserBlackholeInfoDto of(Long userId, String name, String email, LocalDateTime blackHoledAt) { return new UserBlackholeInfoDto(userId, name, email, blackHoledAt); } + public static UserBlackholeInfoDto of(User user) { + return new UserBlackholeInfoDto(user.getUserId(), user.getName(), user.getEmail(), user.getBlackholedAt()); + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/event/BlackholedUserLentEventListener.java b/backend/src/main/java/org/ftclub/cabinet/event/BlackholedUserLentEventListener.java new file mode 100644 index 000000000..b2478601b --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/event/BlackholedUserLentEventListener.java @@ -0,0 +1,24 @@ +package org.ftclub.cabinet.event; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.ftclub.cabinet.dto.UserBlackholeInfoDto; +import org.ftclub.cabinet.utils.blackhole.manager.BlackholeManager; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Log4j2 +@Component +//@EnableAsync +@RequiredArgsConstructor +public class BlackholedUserLentEventListener { + + private final BlackholeManager blackholeManager; + + // @Async + @EventListener + public void handleBlackholedUserLentAttemptingEvent(UserBlackholeInfoDto userBlackholeInfoDto) { + log.info("Called handleBlackholedUserLentAttemptingEvent"); + blackholeManager.handleBlackhole(userBlackholeInfoDto); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/exception/CustomExceptionStatus.java b/backend/src/main/java/org/ftclub/cabinet/exception/CustomExceptionStatus.java new file mode 100644 index 000000000..471523561 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/exception/CustomExceptionStatus.java @@ -0,0 +1,30 @@ +package org.ftclub.cabinet.exception; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +/** + * {@link CustomServiceException}을 위한 exception 클래스. 생성할 exception에 대한 정보를 담고있다. + */ +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +@RequiredArgsConstructor +@Getter +public class CustomExceptionStatus { + private final int statusCode; + private final String message; + private final String error; + + public CustomExceptionStatus(HttpStatus status, String message) { + this.statusCode = status.value(); + this.message = message; + this.error = status.getReasonPhrase(); + } + + public CustomExceptionStatus(ExceptionStatus status, String message) { + this.statusCode = status.getStatusCode(); + this.message = status.getMessage() + "\n" + message; + this.error = status.getError(); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/exception/CustomServiceException.java b/backend/src/main/java/org/ftclub/cabinet/exception/CustomServiceException.java new file mode 100644 index 000000000..578eb53e0 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/exception/CustomServiceException.java @@ -0,0 +1,21 @@ +package org.ftclub.cabinet.exception; + +/** + * Service에서 throw 하는 Exception 중 오류메세지를 커스텀 가능한 Exception + * @see CustomExceptionStatus + */ +public class CustomServiceException extends RuntimeException { + + final CustomExceptionStatus status; + + /** + * @param status exception에 대한 정보에 대한 enum + */ + public CustomServiceException(CustomExceptionStatus status) { + this.status = status; + } + + public CustomExceptionStatus getStatus() { + return status; + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionController.java b/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionController.java index 8c63fbc94..8b11cc9c5 100644 --- a/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionController.java +++ b/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionController.java @@ -25,6 +25,14 @@ public ResponseEntity serviceExceptionHandler(ServiceException e) { .body(e.status); } + @ExceptionHandler(CustomServiceException.class) + public ResponseEntity customServiceExceptionHandler(CustomServiceException e) { + log.info("[CustomServiceException] {} : {}", e.status.getError(), e.status.getMessage()); + return ResponseEntity + .status(e.status.getStatusCode()) + .body(e.status); + } + @ExceptionHandler(DomainException.class) public ResponseEntity domainExceptionHandler(DomainException e) { log.warn("[DomainException] {} : {}", e.status.getError(), e.status.getMessage()); @@ -33,4 +41,12 @@ public ResponseEntity domainExceptionHandler(DomainException e) { .body(e.status); } + @ExceptionHandler(UtilException.class) + public ResponseEntity utilExceptionHandler(UtilException e) { + log.warn("[UtilException] {} : {}", e.status.getError(), e.status.getMessage()); + return ResponseEntity + .status(e.status.getStatusCode()) + .body(e.status); + } + } diff --git a/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java b/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java index 8c4f399a0..97b10e89b 100644 --- a/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java +++ b/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java @@ -34,6 +34,7 @@ public enum ExceptionStatus { SHARE_BANNED_USER(HttpStatus.BAD_REQUEST, "SHARE 밴 상태의 유저입니다."), NOT_FOUND_BAN_HISTORY(HttpStatus.NOT_FOUND, "현재 정지 상태인 유저가 아닙니다."), BLACKHOLED_USER(HttpStatus.BAD_REQUEST, "블랙홀 상태의 유저입니다."), + BLACKHOLE_REFRESHING(HttpStatus.BAD_REQUEST, "블랙홀 갱신 중 입니다.\n잠시 후에 다시 시도해주세요."), UNAUTHORIZED_ADMIN(HttpStatus.UNAUTHORIZED, "관리자 로그인 정보가 유효하지 않습니다\n다시 로그인해주세요"), UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "사용자 로그인 정보가 유효하지 않습니다\n다시 로그인해주세요"), EXTERNAL_API_EXCEPTION(HttpStatus.BAD_REQUEST, "외부 API와 통신 중 에러가 발생했습니다"), @@ -41,7 +42,6 @@ public enum ExceptionStatus { CLUB_HAS_LENT_CABINET(HttpStatus.NOT_ACCEPTABLE, "대여 중인 사물함을 반납 후 삭제할 수 있습니다."), ; - final private int statusCode; final private String message; final private String error; diff --git a/backend/src/main/java/org/ftclub/cabinet/exception/UtilException.java b/backend/src/main/java/org/ftclub/cabinet/exception/UtilException.java new file mode 100644 index 000000000..d626a0fe5 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/exception/UtilException.java @@ -0,0 +1,23 @@ +package org.ftclub.cabinet.exception; + +/** + * Util에서 throw하는 exception들을 위한 exception 사용 예시: + *
+ *     {@code throw new UtilException(ExceptionStatus.NOT_FOUND_USER);}
+ *
+ * 만약 새로운 exception을 만들 필요가 있다면 {@link ExceptionStatus}에서 새로운 enum값을 추가하면 된다. + * @see org.ftclub.cabinet.exception.ExceptionStatus + */ + +public class UtilException extends RuntimeException { + + final ExceptionStatus status; + + /** + * @param status exception에 대한 정보에 대한 enum + */ + public UtilException(ExceptionStatus status) { + this.status = status; + } + +} diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyImpl.java b/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyImpl.java index 776c13c61..a39b8b081 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyImpl.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyImpl.java @@ -1,30 +1,31 @@ package org.ftclub.cabinet.lent.domain; -import java.time.LocalDate; -import java.util.Date; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.ftclub.cabinet.cabinet.domain.Cabinet; import org.ftclub.cabinet.cabinet.domain.CabinetStatus; import org.ftclub.cabinet.cabinet.domain.LentType; import org.ftclub.cabinet.config.CabinetProperties; +import org.ftclub.cabinet.dto.UserBlackholeInfoDto; import org.ftclub.cabinet.exception.DomainException; import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.user.domain.BanHistory; import org.ftclub.cabinet.user.domain.User; import org.ftclub.cabinet.user.domain.UserRole; import org.ftclub.cabinet.utils.DateUtil; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; -import java.util.List; - @Component @RequiredArgsConstructor @Log4j2 public class LentPolicyImpl implements LentPolicy { private final CabinetProperties cabinetProperties; + private final ApplicationEventPublisher publisher; + private LocalDateTime generateSharedCabinetExpirationDate(LocalDateTime now, CabinetStatus cabinetStatus, LentHistory activeLentHistory) { @@ -78,10 +79,11 @@ public LocalDateTime generateExpirationDate(LocalDateTime now, Cabinet cabinet, @Override public void applyExpirationDate(LentHistory curHistory, List beforeActiveHistories, LocalDateTime expiredAt) { - log.info("Called applyExpirationDate curHistory: {}, beforeActiveHistories: {}, expiredAt: {}", + log.info( + "Called applyExpirationDate curHistory: {}, beforeActiveHistories: {}, expiredAt: {}", curHistory, beforeActiveHistories, expiredAt); - if (expiredAt == null){ + if (expiredAt == null) { throw new DomainException(ExceptionStatus.INVALID_ARGUMENT); } @@ -105,17 +107,15 @@ public LentPolicyStatus verifyUserForLent(User user, Cabinet cabinet, int userAc if (userActiveLentCount >= 1) { return LentPolicyStatus.ALREADY_LENT_USER; } -// TODO: 현재 구조에서는 DB 정합성 문제를 일으키는 코드입니다. -// 블랙홀에 빠진 유저 대여 로직을 막는다고 하면, BlackholeManager.handleBlackhole()을 통해 -// 실제 DB에 반영되기 전에 블랙홀에 빠진 유저를 걸러낼 수 있습니다. -// 하지만, 현재는 BlackholeManager <-> LentService 간의 순환 참조가 발생하는데, -// BlackholeManager는 스케줄러에 의해 빈에 등록되는 컴포넌트이므로 -// 현재 구조상으로는 @Lazy 어노테이션을 통해 순환 참조 문제를 해결할 수 없습니다. -// 추후 다른 방식으로 구조적인 리팩토링이 필요한 부분입니다..! -// if (user.getBlackholedAt() != null && user.getBlackholedAt() -// .isBefore(LocalDateTime.now())) { -// return LentPolicyStatus.BLACKHOLED_USER; -// } + if (user.getBlackholedAt() != null && user.getBlackholedAt() + .isBefore(LocalDateTime.now())) { + publisher.publishEvent(UserBlackholeInfoDto.of(user)); + if (user.getBlackholedAt() != null && user.getBlackholedAt() + .isBefore(LocalDateTime.now())) { + return LentPolicyStatus.BLACKHOLED_USER; + } + } + // 유저가 페널티 2 종류 이상 받을 수 있나? <- 실제로 그럴리 없지만 lentPolicy 객체는 그런 사실을 모르고, 유연하게 구현? if (userActiveBanList == null || userActiveBanList.size() == 0) { return LentPolicyStatus.FINE; diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentOptionalFetcher.java b/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentOptionalFetcher.java index fc7c4a846..22a63e185 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentOptionalFetcher.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentOptionalFetcher.java @@ -10,7 +10,6 @@ import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.exception.ServiceException; import org.ftclub.cabinet.lent.domain.LentHistory; -import org.ftclub.cabinet.lent.domain.LentPolicyStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -91,41 +90,6 @@ public LentHistory getActiveLentHistoryWithUserId(Long userId) { .orElseThrow(() -> new ServiceException(ExceptionStatus.NO_LENT_CABINET)); } - /** - * 정책에 대한 결과 상태({@link LentPolicyStatus})에 맞는 적절한 {@link ServiceException}을 throw합니다. - * - * @param status 정책에 대한 결과 상태 - * @throws ServiceException 정책에 따라 다양한 exception이 throw될 수 있습니다. - */ - public void handlePolicyStatus(LentPolicyStatus status) { - log.info("Called handlePolicyStatus status: {}", status); - switch (status) { - case FINE: - break; - case BROKEN_CABINET: - throw new ServiceException(ExceptionStatus.LENT_BROKEN); - case FULL_CABINET: - throw new ServiceException(ExceptionStatus.LENT_FULL); - case OVERDUE_CABINET: - throw new ServiceException(ExceptionStatus.LENT_EXPIRED); - case LENT_CLUB: - throw new ServiceException(ExceptionStatus.LENT_CLUB); - case IMMINENT_EXPIRATION: - throw new ServiceException(ExceptionStatus.LENT_EXPIRE_IMMINENT); - case ALREADY_LENT_USER: - throw new ServiceException(ExceptionStatus.LENT_ALREADY_EXISTED); - case ALL_BANNED_USER: - throw new ServiceException(ExceptionStatus.ALL_BANNED_USER); - case SHARE_BANNED_USER: - throw new ServiceException(ExceptionStatus.SHARE_BANNED_USER); - case BLACKHOLED_USER: - throw new ServiceException(ExceptionStatus.BLACKHOLED_USER); - case NOT_USER: - case INTERNAL_ERROR: - default: - throw new ServiceException(ExceptionStatus.INTERNAL_SERVER_ERROR); - } - } /** * 사물함에 남은 자리가 있는 지 확인합니다. 남은 자리가 없으면 throw합니다. diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentServiceImpl.java b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentServiceImpl.java index 6aac3db9f..09fd839b4 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentServiceImpl.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentServiceImpl.java @@ -1,12 +1,25 @@ package org.ftclub.cabinet.lent.service; +import static org.ftclub.cabinet.exception.ExceptionStatus.ALL_BANNED_USER; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.ftclub.cabinet.cabinet.domain.Cabinet; import org.ftclub.cabinet.cabinet.repository.CabinetOptionalFetcher; import org.ftclub.cabinet.dto.ActiveLentHistoryDto; +import org.ftclub.cabinet.exception.CustomExceptionStatus; +import org.ftclub.cabinet.exception.CustomServiceException; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.exception.ServiceException; import org.ftclub.cabinet.lent.domain.LentHistory; import org.ftclub.cabinet.lent.domain.LentPolicy; +import org.ftclub.cabinet.lent.domain.LentPolicyStatus; import org.ftclub.cabinet.lent.repository.LentOptionalFetcher; import org.ftclub.cabinet.lent.repository.LentRepository; import org.ftclub.cabinet.mapper.LentMapper; @@ -17,147 +30,199 @@ import org.ftclub.cabinet.user.service.UserService; import org.springframework.stereotype.Service; -import javax.transaction.Transactional; -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor @Transactional @Log4j2 public class LentServiceImpl implements LentService { - private final LentRepository lentRepository; - private final LentPolicy lentPolicy; - private final LentOptionalFetcher lentOptionalFetcher; - private final CabinetOptionalFetcher cabinetOptionalFetcher; - private final UserOptionalFetcher userExceptionHandler; - private final UserService userService; - private final BanHistoryRepository banHistoryRepository; - private final LentMapper lentMapper; - - @Override - public void startLentCabinet(Long userId, Long cabinetId) { - log.info("Called startLentCabinet: {}, {}", userId, cabinetId); - LocalDateTime now = LocalDateTime.now(); - Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(cabinetId); - User user = userExceptionHandler.getUser(userId); - int userActiveLentCount = lentRepository.countUserActiveLent(userId); - List userActiveBanList = banHistoryRepository.findUserActiveBanList(userId, - now); - // 대여 가능한 유저인지 확인 - lentOptionalFetcher.handlePolicyStatus( - lentPolicy.verifyUserForLent(user, cabinet, userActiveLentCount, - userActiveBanList)); - List cabinetActiveLentHistories = lentRepository.findAllActiveLentByCabinetId( - cabinetId); - // 대여 가능한 캐비넷인지 확인 - lentOptionalFetcher.handlePolicyStatus( - lentPolicy.verifyCabinetForLent(cabinet, cabinetActiveLentHistories, - now)); - // 캐비넷 상태 변경 - cabinet.specifyStatusByUserCount(cabinetActiveLentHistories.size() + 1); - LocalDateTime expiredAt = lentPolicy.generateExpirationDate(now, cabinet, - cabinetActiveLentHistories); - LentHistory lentHistory = LentHistory.of(now, expiredAt, userId, cabinetId); - // 연체 시간 적용 - lentPolicy.applyExpirationDate(lentHistory, cabinetActiveLentHistories, expiredAt); - lentRepository.save(lentHistory); - } - - @Override - public void startLentClubCabinet(Long userId, Long cabinetId) { - log.debug("Called startLentClubCabinet: {}, {}", userId, cabinetId); - Cabinet cabinet = cabinetOptionalFetcher.getClubCabinet(cabinetId); - lentOptionalFetcher.checkExistedSpace(cabinetId); - LocalDateTime expirationDate = lentPolicy.generateExpirationDate(LocalDateTime.now(), - cabinet, null); - LentHistory result = - LentHistory.of(LocalDateTime.now(), expirationDate, userId, cabinetId); - lentRepository.save(result); - cabinet.specifyStatusByUserCount(1); // todo : policy에서 관리 - } - - @Override - public void endLentCabinet(Long userId) { - log.debug("Called endLentCabinet: {}", userId); - LentHistory lentHistory = returnCabinetByUserId(userId); - Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(lentHistory.getCabinetId()); - // cabinetType도 인자로 전달하면 좋을 거 같습니다 (공유사물함 3일이내 반납 페널티) - userService.banUser(userId, cabinet.getLentType(), lentHistory.getStartedAt(), - lentHistory.getEndedAt(), lentHistory.getExpiredAt()); - } - - @Override - public void terminateLentCabinet(Long userId) { - log.debug("Called terminateLentCabinet: {}", userId); - returnCabinetByUserId(userId); - } - - @Override - public void terminateLentByCabinetId(Long cabinetId) { - log.debug("Called terminateLentByCabinetId: {}", cabinetId); - returnCabinetByCabinetId(cabinetId); - } - - // cabinetId로 return하는 경우에서, 공유 사물함과 개인 사물함의 경우에 대한 분기가 되어 있지 않음. - // 또한 어드민의 경우에서 사용하는 returnByCabinetId와 유저가 사용하는 returnByCabinetId가 다른 상황이므로 - // (어드민의 경우에는 뭐든지 전체 반납, 유저가 사용하는 경우에는 본인이 사용하는 사물함에 대한 반납) - // 유저가 사용하는 경우에 대해서는 userId로만 쓰게하든, 한 방식으로만 사용하게끔 해야함 - 함수를 쪼갤 가능성도 있음. - // 우선 현재 관리자만 쓰고 있고, 한 군데에서만 사용되므로 List로 전체 반납을 하도록 구현, 이에 대한 논의는 TO-DO - private List returnCabinetByCabinetId(Long cabinetId) { - log.debug("Called returnCabinetByCabinetId: {}", cabinetId); - Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(cabinetId); - List lentHistories = lentOptionalFetcher.findAllActiveLentByCabinetId( - cabinetId); - lentHistories.forEach(lentHistory -> lentHistory.endLent(LocalDateTime.now())); - cabinet.specifyStatusByUserCount(0); // policy로 빼는게..? - cabinet.writeMemo(""); - cabinet.writeTitle(""); - return lentHistories; - } - - private LentHistory returnCabinetByUserId(Long userId) { - log.debug("Called returnCabinet: {}", userId); - userExceptionHandler.getUser(userId); - LentHistory lentHistory = lentOptionalFetcher.getActiveLentHistoryWithUserId(userId); - Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(lentHistory.getCabinetId()); - int activeLentCount = lentRepository.countCabinetActiveLent(lentHistory.getCabinetId()); - lentHistory.endLent(LocalDateTime.now()); - cabinet.specifyStatusByUserCount(activeLentCount - 1); // policy로 빠질만한 부분인듯? - if (activeLentCount - 1 == 0) { - cabinet.writeMemo(""); - cabinet.writeTitle(""); - } - return lentHistory; - } - - @Override - public void assignLent(Long userId, Long cabinetId) { - log.debug("Called assignLent: {}, {}", userId, cabinetId); - userExceptionHandler.getUser(userId); - Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(cabinetId); - lentOptionalFetcher.checkExistedSpace(cabinetId); - LocalDateTime expirationDate = lentPolicy.generateExpirationDate(LocalDateTime.now(), - cabinet, null); - LentHistory result = LentHistory.of(LocalDateTime.now(), expirationDate, userId, cabinetId); - cabinet.specifyStatusByUserCount(1); - lentRepository.save(result); - } - - @Override - public List getAllActiveLentHistories() { - log.debug("Called getAllActiveLentHistories"); - List lentHistories = lentOptionalFetcher.findAllActiveLentHistories(); - LocalDateTime now = LocalDateTime.now(); - return lentHistories.stream() - .map(e -> lentMapper.toActiveLentHistoryDto(e, - e.getUser(), - e.getCabinet(), - e.isExpired(now), - e.getDaysUntilExpiration(now) - )) - .collect(Collectors.toList()); - } + private final LentRepository lentRepository; + private final LentPolicy lentPolicy; + private final LentOptionalFetcher lentOptionalFetcher; + private final CabinetOptionalFetcher cabinetOptionalFetcher; + private final UserOptionalFetcher userOptionalFetcher; + private final UserService userService; + private final BanHistoryRepository banHistoryRepository; + private final LentMapper lentMapper; + + @Override + public void startLentCabinet(Long userId, Long cabinetId) { + log.info("Called startLentCabinet: {}, {}", userId, cabinetId); + LocalDateTime now = LocalDateTime.now(); + Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(cabinetId); + User user = userOptionalFetcher.getUser(userId); + int userActiveLentCount = lentRepository.countUserActiveLent(userId); + List userActiveBanList = banHistoryRepository.findUserActiveBanList(userId, + now); + // 대여 가능한 유저인지 확인 + LentPolicyStatus userPolicyStatus = lentPolicy.verifyUserForLent(user, cabinet, + userActiveLentCount, userActiveBanList); + handlePolicyStatus(userPolicyStatus, userActiveBanList); // UserPolicyStatus 와 LentPolicyStatus 가 분리해야 하지않는가? 23/8/15 + List cabinetActiveLentHistories = lentRepository.findAllActiveLentByCabinetId( + cabinetId); + + // 대여 가능한 캐비넷인지 확인 + LentPolicyStatus cabinetPolicyStatus = lentPolicy.verifyCabinetForLent(cabinet, + cabinetActiveLentHistories, + now); + handlePolicyStatus(cabinetPolicyStatus, userActiveBanList); // UserPolicyStatus 와 LentPolicyStatus 가 분리해야 하지않는가? 23/8/15 + + // 캐비넷 상태 변경 + cabinet.specifyStatusByUserCount(cabinetActiveLentHistories.size() + 1); + LocalDateTime expiredAt = lentPolicy.generateExpirationDate(now, cabinet, + cabinetActiveLentHistories); + LentHistory lentHistory = LentHistory.of(now, expiredAt, userId, cabinetId); + + // 연체 시간 적용 + lentPolicy.applyExpirationDate(lentHistory, cabinetActiveLentHistories, expiredAt); + lentRepository.save(lentHistory); + } + + @Override + public void startLentClubCabinet(Long userId, Long cabinetId) { + log.debug("Called startLentClubCabinet: {}, {}", userId, cabinetId); + Cabinet cabinet = cabinetOptionalFetcher.getClubCabinet(cabinetId); + lentOptionalFetcher.checkExistedSpace(cabinetId); + LocalDateTime expirationDate = lentPolicy.generateExpirationDate(LocalDateTime.now(), + cabinet, null); + LentHistory result = + LentHistory.of(LocalDateTime.now(), expirationDate, userId, cabinetId); + lentRepository.save(result); + cabinet.specifyStatusByUserCount(1); // todo : policy에서 관리 + } + + @Override + public void endLentCabinet(Long userId) { + log.debug("Called endLentCabinet: {}", userId); + LentHistory lentHistory = returnCabinetByUserId(userId); + Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(lentHistory.getCabinetId()); + // cabinetType도 인자로 전달하면 좋을 거 같습니다 (공유사물함 3일이내 반납 페널티) + userService.banUser(userId, cabinet.getLentType(), lentHistory.getStartedAt(), + lentHistory.getEndedAt(), lentHistory.getExpiredAt()); + } + + @Override + public void terminateLentCabinet(Long userId) { + log.debug("Called terminateLentCabinet: {}", userId); + returnCabinetByUserId(userId); + } + + @Override + public void terminateLentByCabinetId(Long cabinetId) { + log.debug("Called terminateLentByCabinetId: {}", cabinetId); + returnCabinetByCabinetId(cabinetId); + } + + // cabinetId로 return하는 경우에서, 공유 사물함과 개인 사물함의 경우에 대한 분기가 되어 있지 않음. + // 또한 어드민의 경우에서 사용하는 returnByCabinetId와 유저가 사용하는 returnByCabinetId가 다른 상황이므로 + // (어드민의 경우에는 뭐든지 전체 반납, 유저가 사용하는 경우에는 본인이 사용하는 사물함에 대한 반납) + // 유저가 사용하는 경우에 대해서는 userId로만 쓰게하든, 한 방식으로만 사용하게끔 해야함 - 함수를 쪼갤 가능성도 있음. + // 우선 현재 관리자만 쓰고 있고, 한 군데에서만 사용되므로 List로 전체 반납을 하도록 구현, 이에 대한 논의는 TO-DO + private List returnCabinetByCabinetId(Long cabinetId) { + log.debug("Called returnCabinetByCabinetId: {}", cabinetId); + Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(cabinetId); + List lentHistories = lentOptionalFetcher.findAllActiveLentByCabinetId( + cabinetId); + lentHistories.forEach(lentHistory -> lentHistory.endLent(LocalDateTime.now())); + cabinet.specifyStatusByUserCount(0); // policy로 빼는게..? + cabinet.writeMemo(""); + cabinet.writeTitle(""); + return lentHistories; + } + + private LentHistory returnCabinetByUserId(Long userId) { + log.debug("Called returnCabinet: {}", userId); + userOptionalFetcher.getUser(userId); + LentHistory lentHistory = lentOptionalFetcher.getActiveLentHistoryWithUserId(userId); + Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(lentHistory.getCabinetId()); + int activeLentCount = lentRepository.countCabinetActiveLent(lentHistory.getCabinetId()); + lentHistory.endLent(LocalDateTime.now()); + cabinet.specifyStatusByUserCount(activeLentCount - 1); // policy로 빠질만한 부분인듯? + if (activeLentCount - 1 == 0) { + cabinet.writeMemo(""); + cabinet.writeTitle(""); + } + return lentHistory; + } + + @Override + public void assignLent(Long userId, Long cabinetId) { + log.debug("Called assignLent: {}, {}", userId, cabinetId); + userOptionalFetcher.getUser(userId); + Cabinet cabinet = cabinetOptionalFetcher.getCabinetForUpdate(cabinetId); + lentOptionalFetcher.checkExistedSpace(cabinetId); + LocalDateTime expirationDate = lentPolicy.generateExpirationDate(LocalDateTime.now(), + cabinet, null); + LentHistory result = LentHistory.of(LocalDateTime.now(), expirationDate, userId, cabinetId); + cabinet.specifyStatusByUserCount(1); + lentRepository.save(result); + } + + @Override + public List getAllActiveLentHistories() { + log.debug("Called getAllActiveLentHistories"); + List lentHistories = lentOptionalFetcher.findAllActiveLentHistories(); + LocalDateTime now = LocalDateTime.now(); + return lentHistories.stream() + .map(e -> lentMapper.toActiveLentHistoryDto(e, + e.getUser(), + e.getCabinet(), + e.isExpired(now), + e.getDaysUntilExpiration(now) + )) + .collect(Collectors.toList()); + } + + /** + * 정책에 대한 결과 상태({@link LentPolicyStatus})에 맞는 적절한 {@link ServiceException}을 throw합니다. + * + * @param status 정책에 대한 결과 상태 + * @throws ServiceException 정책에 따라 다양한 exception이 throw될 수 있습니다. + */ + private void handlePolicyStatus(LentPolicyStatus status, List banHistory) { + log.info("Called handlePolicyStatus status: {}", status); + switch (status) { + case FINE: + break; + case BROKEN_CABINET: + throw new ServiceException(ExceptionStatus.LENT_BROKEN); + case FULL_CABINET: + throw new ServiceException(ExceptionStatus.LENT_FULL); + case OVERDUE_CABINET: + throw new ServiceException(ExceptionStatus.LENT_EXPIRED); + case LENT_CLUB: + throw new ServiceException(ExceptionStatus.LENT_CLUB); + case IMMINENT_EXPIRATION: + throw new ServiceException(ExceptionStatus.LENT_EXPIRE_IMMINENT); + case ALREADY_LENT_USER: + throw new ServiceException(ExceptionStatus.LENT_ALREADY_EXISTED); + case ALL_BANNED_USER: + case SHARE_BANNED_USER: + handleBannedUserResponse(status, banHistory.get(0)); + case BLACKHOLED_USER: + throw new ServiceException(ExceptionStatus.BLACKHOLED_USER); + case NOT_USER: + case INTERNAL_ERROR: + default: + throw new ServiceException(ExceptionStatus.INTERNAL_SERVER_ERROR); + } + } + + private void handleBannedUserResponse(LentPolicyStatus status, BanHistory banHistory) { + log.info("Called handleBannedUserResponse: {}", status); + + + LocalDateTime unbannedAt = banHistory.getUnbannedAt(); + String unbannedTimeString = unbannedAt.format( + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + + if (status.equals(LentPolicyStatus.ALL_BANNED_USER)) { + throw new CustomServiceException( + new CustomExceptionStatus(ExceptionStatus.ALL_BANNED_USER, unbannedTimeString)); + } else if (status.equals(LentPolicyStatus.SHARE_BANNED_USER)) { + throw new CustomServiceException( + new CustomExceptionStatus(ExceptionStatus.SHARE_BANNED_USER, + unbannedTimeString)); + } + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/repository/UserOptionalFetcher.java b/backend/src/main/java/org/ftclub/cabinet/user/repository/UserOptionalFetcher.java index 0386d7e44..69e7b0527 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/repository/UserOptionalFetcher.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/repository/UserOptionalFetcher.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.ftclub.cabinet.exception.DomainException; @@ -238,4 +239,12 @@ public BanHistory getRecentBanHistory(Long userId) { } return banHistory.get(0); } + + public BanHistory getActiveBanHistory(Long userId) { + log.debug("Called getActiveBanHistory: {}", userId); + Optional banHistory = banHistoryRepository.findRecentActiveBanHistoryByUserId( + userId, + LocalDateTime.now()); + return banHistory.get(); + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java b/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java index 2c3b7ee93..92fcb5379 100644 --- a/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java +++ b/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java @@ -8,6 +8,7 @@ import org.ftclub.cabinet.dto.UserBlackholeInfoDto; import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.exception.ServiceException; +import org.ftclub.cabinet.exception.UtilException; import org.ftclub.cabinet.lent.service.LentService; import org.ftclub.cabinet.user.service.UserService; import org.springframework.http.HttpStatus; @@ -19,7 +20,7 @@ @Log4j2 public class BlackholeManager { - private final FtApiManager ftAPIManager; + private final FtApiManager ftApiManager; private final LentService lentService; private final UserService userService; @@ -30,8 +31,9 @@ public class BlackholeManager { * @param jsonUserInfo JsonNode에 담긴 유저 정보 * @return 카뎃 여부 */ - private Boolean isValidCadet(JsonNode jsonUserInfo) { - log.info("isValidCadet {}", jsonUserInfo); + private boolean isValidCadet(JsonNode jsonUserInfo) { + log.debug("isValidCadet {}", jsonUserInfo); + log.info("isValidCadet"); return jsonUserInfo.get("cursus_users").size() >= 2; } @@ -54,11 +56,11 @@ private LocalDateTime parseBlackholedAt(JsonNode jsonUserInfo) { * 갱신된 블랙홀 날짜를 바탕으로 블랙홀에 빠졌는지 확인한다. * * @param blackholedAtDate 블랙홀 날짜 - * @param now 현재 시간 * @return 블랙홀에 빠졌는지 여부 */ - private Boolean isBlackholed(LocalDateTime blackholedAtDate, LocalDateTime now) { - log.info("isBlackholed {} {}", blackholedAtDate, now); + private boolean isBlackholed(LocalDateTime blackholedAtDate) { + log.info("isBlackholed {} {}", blackholedAtDate); + LocalDateTime now = LocalDateTime.now(); if (blackholedAtDate == null || blackholedAtDate.isAfter(now)) { return false; } else { @@ -82,10 +84,10 @@ private void handleNotCadet(UserBlackholeInfoDto userBlackholeInfoDto, LocalDate * 블랙홀에 빠진 유저를 강제 반납 및 삭제 처리한다. * * @param userBlackholeInfoDto 유저 정보 {@link UserBlackholeInfoDto} - * @param now 현재 시간 */ - private void handleBlackholed(UserBlackholeInfoDto userBlackholeInfoDto, LocalDateTime now) { + private void handleBlackholed(UserBlackholeInfoDto userBlackholeInfoDto) { log.info("{}는 블랙홀에 빠졌습니다.", userBlackholeInfoDto); + LocalDateTime now = LocalDateTime.now(); lentService.terminateLentCabinet(userBlackholeInfoDto.getUserId()); userService.deleteUser(userBlackholeInfoDto.getUserId(), now); } @@ -119,33 +121,102 @@ private void handleHttpClientError(UserBlackholeInfoDto userBlackholeInfoDto, Lo } } - public void handleBlackhole(UserBlackholeInfoDto userBlackholeInfoDto) { - log.info("called handleBlackhole {}", userBlackholeInfoDto); + /** + * 유저의 블랙홀 정보를 API 를 통해 요청하여 찾아온다. + * + * @param userName 유저 이름 + * @return JsonNode 유저의 블랙홀 정보 + * @throws ServiceException + */ + private JsonNode getBlackholeInfo(String userName) + throws ServiceException, HttpClientErrorException { + log.info("called refreshBlackhole{}", userName); + JsonNode userInfoFromIntra = ftApiManager.getFtUsersInfoByName( + userName); + + return userInfoFromIntra; + } + + + /** + * 유저의 블랙홀 날짜를 갱신하여 LocalDateTime으로 반환한다. + * + * @param userName 유저 이름 + * @return 갱신된 블랙홀 날짜 LocalDateTime + */ + private LocalDateTime refreshBlackholedAt(String userName) { + log.info("refreshBlackholedAt {}", userName); + JsonNode blackholeInfo = getBlackholeInfo(userName); + return parseBlackholedAt(blackholeInfo); + } + + /** + * 유저속성의 블랙홀 날짜를 갱신한다. + * + * @param userId 유저 아이디 + * @param blackholedAt 갱신할 블랙홀 날짜 + */ + private void updateUserBlackholedAt(Long userId, LocalDateTime blackholedAt) { + userService.updateUserBlackholedAt(userId, blackholedAt); + } + + + /** + * 스케쥴러가 샐행하는 블랙홀 처리 메서드 유저의 블랙홀 정보를 갱신하여 블랙홀에 빠졌는지 확인 후 처리한다. + *

+ * 블랙홀에 빠진경우 반납 / 계정 삭제처리 블랙홀에 빠지지 않은경우 블랙홀 날짜 갱신 + * + * @param userInfoDto + */ + + public void handleBlackhole(UserBlackholeInfoDto userInfoDto) { + log.info("called handleBlackhole {}", userInfoDto); LocalDateTime now = LocalDateTime.now(); try { - JsonNode jsonUserInfo = ftAPIManager.getFtUsersInfoByName( - userBlackholeInfoDto.getName()); - if (!isValidCadet(jsonUserInfo)) { - handleNotCadet(userBlackholeInfoDto, now); + JsonNode jsonRefreshedUserInfo = getBlackholeInfo(userInfoDto.getName()); + if (!isValidCadet(jsonRefreshedUserInfo)) { + handleNotCadet(userInfoDto, now); return; } - LocalDateTime newBlackholedAt = parseBlackholedAt(jsonUserInfo); + LocalDateTime newBlackholedAt = parseBlackholedAt(jsonRefreshedUserInfo); log.info("갱신된 블랙홀 날짜 {}", newBlackholedAt); log.info("오늘 날짜 {}", now); - if (isBlackholed(newBlackholedAt, now)) { - handleBlackholed(userBlackholeInfoDto, now); + if (isBlackholed(newBlackholedAt)) { + handleBlackholed(userInfoDto); } else { - handleNotBlackholed(userBlackholeInfoDto, newBlackholedAt); + handleNotBlackholed(userInfoDto, newBlackholedAt); } } catch (HttpClientErrorException e) { - handleHttpClientError(userBlackholeInfoDto, now, e); + handleHttpClientError(userInfoDto, now, e); } catch (ServiceException e) { if (e.getStatus().equals(ExceptionStatus.NO_LENT_CABINET)) { - userService.deleteUser(userBlackholeInfoDto.getUserId(), now); + userService.deleteUser(userInfoDto.getUserId(), now); } + else if (e.getStatus().equals(ExceptionStatus.OAUTH_BAD_GATEWAY)) + log.info("handleBlackhole ServiceException {}", e.getStatus()); + throw new UtilException(e.getStatus()); } catch (Exception e) { - log.error("handleBlackhole Exception: {}", userBlackholeInfoDto, e); + log.error("handleBlackhole Exception: {}", userInfoDto, e); + } + } + + // 따로 분리할 필요 없을듯.. + + /** + * 블랙홀 갱신 후 처리 + *

+ * 블랙홀일 경우 반납 및 삭제 처리 블랙홀이 아닐경우 유저 정보(블랙홀일자) 업데이트 + * + * @param userBlackholeInfoDto + */ + public void blackholeRefresher(UserBlackholeInfoDto userBlackholeInfoDto) { + LocalDateTime refreshedBlackholedAt = refreshBlackholedAt(userBlackholeInfoDto.getName()); + if (isBlackholed(refreshedBlackholedAt)) { + handleBlackholed(userBlackholeInfoDto); + throw new ServiceException(ExceptionStatus.BLACKHOLED_USER); + } else { + updateUserBlackholedAt(userBlackholeInfoDto.getUserId(), refreshedBlackholedAt); } } } diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java b/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java index 00c1ec84d..7a49c5d3e 100644 --- a/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java +++ b/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java @@ -29,8 +29,10 @@ public class SystemScheduler { private final UserService userService; private final BlackholeManager blackholeManager; + private static final long DELAY_TIME = 2000; + /** - * 매일 자정마다 대여 기록을 확인하여, 연체 메일 발송 및 휴학생 처리를 트리거하는 메소드 + * 매일 자정마다 대여 기록을 확인하여, 연체 메일 발송 및 휴학생 처리를 트리거하는 메소드 2초 간격으로 블랙홀 검증 */ @Scheduled(cron = "${spring.schedule.cron.leave-absence}") public void checkAllLents() { @@ -39,9 +41,8 @@ public void checkAllLents() { for (ActiveLentHistoryDto activeLent : activeLents) { overdueManager.handleOverdue(activeLent); leaveAbsenceManager.handleLeaveAbsence(activeLent.getUserId(), activeLent.getName()); - // 2초 간격으로 대여 검증 try { - Thread.sleep(2000); + Thread.sleep(DELAY_TIME); } catch (InterruptedException e) { log.error(e.getMessage()); } @@ -49,7 +50,7 @@ public void checkAllLents() { } /** - * 매주 월요일 자정 42분에 블랙홀에 빠진 유저 처리를 트리거하는 메소드 + * 매주 월요일 자정 42분에 블랙홀에 빠진 유저 처리를 트리거하는 메소드 2초 간격으로 블랙홀 검증 */ @Scheduled(cron = "${spring.schedule.cron.risk-of-blackhole}") public void checkRiskOfBlackhole() { @@ -57,9 +58,8 @@ public void checkRiskOfBlackhole() { List blackholeInfos = userService.getAllRiskOfBlackholeInfo(); for (UserBlackholeInfoDto blackholeInfo : blackholeInfos) { blackholeManager.handleBlackhole(blackholeInfo); - // 2초 간격으로 블랙홀 검증 try { - Thread.sleep(2000); + Thread.sleep(DELAY_TIME); } catch (InterruptedException e) { log.error(e.getMessage()); } @@ -67,7 +67,7 @@ public void checkRiskOfBlackhole() { } /** - * 매월 1일 01시 42분에 블랙홀에 빠질 위험이 없는 유저들의 블랙홀 처리를 트리거하는 메소드 + * 매월 1일 01시 42분에 블랙홀에 빠질 위험이 없는 유저들의 블랙홀 처리를 트리거하는 메소드 2초 간격으로 블랙홀 검증 */ @Scheduled(cron = "${spring.schedule.cron.no-risk-of-blackhole}") public void checkNoRiskOfBlackhole() { @@ -75,9 +75,8 @@ public void checkNoRiskOfBlackhole() { List blackholeInfos = userService.getAllNoRiskOfBlackholeInfo(); for (UserBlackholeInfoDto blackholeInfo : blackholeInfos) { blackholeManager.handleBlackhole(blackholeInfo); - // 2초 간격으로 블랙홀 검증 try { - Thread.sleep(2000); + Thread.sleep(DELAY_TIME); } catch (InterruptedException e) { log.error(e.getMessage()); } diff --git a/backend/src/main/resources/log4j2-local.yml b/backend/src/main/resources/log4j2-local.yml index 83f5a42a6..7f1087801 100644 --- a/backend/src/main/resources/log4j2-local.yml +++ b/backend/src/main/resources/log4j2-local.yml @@ -18,13 +18,13 @@ Configuration: - name: org.springframework # local 환경에서는 debug, production 환경에서는 info - level: debug + level: info additivity: false AppenderRef: - ref: console - name: org.ftclub.cabinet - level: debug + level: info additivity: false AppenderRef: - ref: console diff --git a/backend/src/test/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManagerUnitTest.java b/backend/src/test/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManagerUnitTest.java index 1fbebd57c..f1b6746fc 100644 --- a/backend/src/test/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManagerUnitTest.java +++ b/backend/src/test/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManagerUnitTest.java @@ -17,12 +17,8 @@ import org.ftclub.cabinet.auth.service.FtApiManager; import org.ftclub.cabinet.config.FtApiProperties; import org.ftclub.cabinet.dto.UserBlackholeInfoDto; -import org.ftclub.cabinet.exception.ExceptionStatus; -import org.ftclub.cabinet.exception.ServiceException; import org.ftclub.cabinet.lent.service.LentService; import org.ftclub.cabinet.user.service.UserService; -import org.ftclub.cabinet.utils.ExceptionUtil; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test;