From 13e73efa55660667b1f3548e0c99814f68881564 Mon Sep 17 00:00:00 2001 From: yushin Date: Sat, 3 May 2025 14:49:06 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feature/=EC=98=88=EC=95=BD=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=98=88=EC=95=BD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/queue/StoreQueueService.java | 10 ++++ .../application/reserve/ReserveService.java | 36 ++++++++++++ .../reserve/UserReserveService.java | 57 +++++++++++++++++++ .../reserve/dto/StoreReserveRegisterDto.java | 11 ++++ .../reserve/mapper/StoreReserveMapper.java | 19 +++++++ .../reserve/mapper/UserReserveDataMapper.java | 27 +++++++++ .../reserve/entity/UserReserveData.java | 16 ++++++ .../reserve/entity/value/ReservePayType.java | 27 +++++++++ .../reserve/entity/value/ReserveStatus.java | 4 ++ .../reserve/repository/ReserveRepository.java | 9 +++ .../repository/UserReserveDataRepository.java | 7 +++ .../catchtable/domain/user/entity/User.java | 2 + .../catchtable/exception/BadRequestError.java | 9 ++- .../reserve/ReserveController.java | 18 ++++-- .../reserve/dto/StoreReserveRequestDto.java | 11 ++++ .../repository/ReserveRepositoryTest.java | 6 ++ 16 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java create mode 100644 src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java create mode 100644 src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java create mode 100644 src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java create mode 100644 src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java create mode 100644 src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java create mode 100644 src/main/java/com/yscp/catchtable/presentation/reserve/dto/StoreReserveRequestDto.java diff --git a/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java index c9c2b6f..d2a1f49 100644 --- a/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java +++ b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java @@ -13,4 +13,14 @@ public class StoreQueueService { public void registerWaiting(StoreQueueDto storeQueueDto) { redisTemplate.opsForZSet().add(storeQueueDto.key(), storeQueueDto.value(), storeQueueDto.score()); } + + public boolean isValidTicket(StoreQueueDto storeQueueDto) { + Double score = redisTemplate.opsForZSet().score(storeQueueDto.key(), storeQueueDto.value()); + long now = System.currentTimeMillis(); + return now - score <= 7 * 60 * 1000; + } + + public void delete(StoreQueueDto storeQueueDto) { + redisTemplate.opsForZSet().remove(storeQueueDto.key(), storeQueueDto.value()); + } } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java b/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java index 9b76fb0..48c3187 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java @@ -1,9 +1,14 @@ package com.yscp.catchtable.application.reserve; +import com.yscp.catchtable.application.queue.StoreQueueService; +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; import com.yscp.catchtable.application.reserve.dto.ReserveDto; import com.yscp.catchtable.application.reserve.dto.StoreReserveDto; +import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; import com.yscp.catchtable.domain.reserve.entity.ReserveData; import com.yscp.catchtable.domain.reserve.repository.ReserveRepository; +import com.yscp.catchtable.exception.BadRequestError; +import com.yscp.catchtable.exception.CatchTableException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,6 +16,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Transactional @@ -18,6 +24,7 @@ @Service public class ReserveService { private final ReserveRepository repository; + private final StoreQueueService storeQueueService; public Map> findReserveDtoMapByStores(List idxes, LocalDate maxDate) { List storeReserveDtos = repository.findStoreReserveDtoListBeforeMaxDate(idxes, maxDate); @@ -36,4 +43,33 @@ public ReservesInDayDto getReservesInDay(Long storeIdx, LocalDate date) { List reserveDates = repository.findByStore_IdxAndReserveDate(storeIdx, date); return ReservesInDayDto.from(reserveDates); } + + public void reserve(StoreReserveRegisterDto dto) { + StoreQueueDto storeQueueDto = new StoreQueueDto(dto.storeReserveIdx().toString(), dto.userIdx().toString()); + // 캐시 조회 유효기간 남았는지 + if (ticketValid(storeQueueDto)) { + throw new CatchTableException(BadRequestError.EXPIRED_TICKET); + } + + Optional reserveDataOptional = repository.findWithStoreByIdx(dto.storeReserveIdx()); + reserveDataOptional.ifPresentOrElse( + reserveData -> { + + }, + () -> { + throw new CatchTableException(BadRequestError.NULL_EXCEPTION); + } + ); + // 스토어 및 유저 스토어 조회 + // 유저 예약 데이터 등록 + // 캐쉬 삭제 + } + + private boolean ticketValid(StoreQueueDto queueDto) { + return storeQueueService.isValidTicket(queueDto); + } + + public Optional findWithStoreByIdx(Long storeReserveIdx) { + return repository.findWithStoreByIdx(storeReserveIdx); + } } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java new file mode 100644 index 0000000..4f385bb --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java @@ -0,0 +1,57 @@ +package com.yscp.catchtable.application.reserve; + +import com.yscp.catchtable.application.queue.StoreQueueService; +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; +import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; +import com.yscp.catchtable.application.reserve.mapper.UserReserveDataMapper; +import com.yscp.catchtable.domain.reserve.entity.ReserveData; +import com.yscp.catchtable.domain.reserve.entity.UserReserveData; +import com.yscp.catchtable.domain.reserve.repository.UserReserveDataRepository; +import com.yscp.catchtable.exception.BadRequestError; +import com.yscp.catchtable.exception.CatchTableException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Transactional +@RequiredArgsConstructor +@Service +public class UserReserveService { + private final StoreQueueService storeQueueService; + private final ReserveService reserveService; + private final UserReserveDataRepository userReserveDataRepository; + + public void reserve(StoreReserveRegisterDto dto) { + StoreQueueDto storeQueueDto = new StoreQueueDto(dto.storeReserveIdx().toString(), dto.userIdx().toString()); + + if (ticketValid(storeQueueDto)) { + throw new CatchTableException(BadRequestError.EXPIRED_TICKET); + } + + Optional reserveDataOptional = reserveService.findWithStoreByIdx(dto.storeReserveIdx()); + + reserveDataOptional.ifPresentOrElse( + reserveData -> { + saveUserReserveData(dto, reserveData, storeQueueDto); + }, + () -> { + throw new CatchTableException(BadRequestError.NULL_EXCEPTION); + } + ); + } + + private void saveUserReserveData(StoreReserveRegisterDto dto, ReserveData reserveData, StoreQueueDto storeQueueDto) { + UserReserveData userReserveData = UserReserveDataMapper.toEntity(reserveData, dto); + if (userReserveData != null) { + userReserveDataRepository.save(userReserveData); + } + // 티켓 삭제 + storeQueueService.delete(storeQueueDto); + } + + private boolean ticketValid(StoreQueueDto queueDto) { + return storeQueueService.isValidTicket(queueDto); + } +} diff --git a/src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java b/src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java new file mode 100644 index 0000000..235c5a0 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java @@ -0,0 +1,11 @@ +package com.yscp.catchtable.application.reserve.dto; + +public record StoreReserveRegisterDto( + Long userIdx, + Long storeReserveIdx, + String reserveType, + String transactionNo, + String purpose, + Integer reservationNumberOfPeople +) { +} diff --git a/src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java b/src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java new file mode 100644 index 0000000..f8fea1d --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java @@ -0,0 +1,19 @@ +package com.yscp.catchtable.application.reserve.mapper; + +import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; +import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class StoreReserveMapper { + public static StoreReserveRegisterDto toDto(StoreReserveRequestDto storeReserveRequestDto) { + return new StoreReserveRegisterDto( + storeReserveRequestDto.userIdx(), + storeReserveRequestDto.storeReserveIdx(), + storeReserveRequestDto.reserveType(), + storeReserveRequestDto.transactionNo(), + storeReserveRequestDto.purpose(), + storeReserveRequestDto.reservationNumberOfPeople() + ); + } +} diff --git a/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java b/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java new file mode 100644 index 0000000..27e148c --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java @@ -0,0 +1,27 @@ +package com.yscp.catchtable.application.reserve.mapper; + +import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; +import com.yscp.catchtable.domain.reserve.entity.ReserveData; +import com.yscp.catchtable.domain.reserve.entity.UserReserveData; +import com.yscp.catchtable.domain.reserve.entity.value.ReserveStatus; +import com.yscp.catchtable.domain.reserve.entity.value.ReservePayType; +import com.yscp.catchtable.domain.user.entity.User; + +import java.time.LocalDateTime; + +public class UserReserveDataMapper { + public static UserReserveData toEntity(ReserveData reserveData, StoreReserveRegisterDto dto) { + User requestUser = User.builder() + .idx(dto.userIdx()) + .build(); + + return UserReserveData.builder() + .user(requestUser) + .reserveData(reserveData) + .reserveStatus(ReserveStatus.RESERVE) + .reserveType(ReservePayType.from(dto.reserveType())) + .regIdx(dto.userIdx()) + .regDatetime(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserveData.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserveData.java index cd05761..77aa170 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserveData.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserveData.java @@ -1,9 +1,11 @@ package com.yscp.catchtable.domain.reserve.entity; +import com.yscp.catchtable.domain.reserve.entity.value.ReservePayType; import com.yscp.catchtable.domain.reserve.entity.value.ReserveStatus; import com.yscp.catchtable.domain.user.entity.User; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -28,9 +30,23 @@ public class UserReserveData { @Enumerated(EnumType.STRING) private ReserveStatus reserveStatus; + @Enumerated(EnumType.STRING) + private ReservePayType reservePayType; private LocalDateTime regDatetime; private Long regIdx; private LocalDateTime modDatetime; private Long modIdx; + @Builder + public UserReserveData(Long idx, User user, ReserveData reserveData, ReserveStatus reserveStatus, ReservePayType reservePayType, LocalDateTime regDatetime, Long regIdx, LocalDateTime modDatetime, Long modIdx) { + this.idx = idx; + this.user = user; + this.reserveData = reserveData; + this.reserveStatus = reserveStatus; + this.reservePayType = reservePayType; + this.regDatetime = regDatetime; + this.regIdx = regIdx; + this.modDatetime = modDatetime; + this.modIdx = modIdx; + } } diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java new file mode 100644 index 0000000..ae98d4d --- /dev/null +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java @@ -0,0 +1,27 @@ +package com.yscp.catchtable.domain.reserve.entity.value; + +import com.yscp.catchtable.exception.BadRequestError; +import com.yscp.catchtable.exception.CatchTableException; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum ReservePayType { + CARD, + CATCH_PAY; + + private static final Map MAPPING; + + static { + MAPPING = Arrays.stream(ReservePayType.values()) + .collect(Collectors.toMap(Enum::name, e -> e)); + } + + public static ReservePayType from(String reserveTypeString) { + if (reserveTypeString == null || reserveTypeString.isEmpty()) { + throw new CatchTableException(BadRequestError.INVALID_RESERVE_PAY_TYPE); + } + return MAPPING.get(reserveTypeString.toUpperCase()); + } +} diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReserveStatus.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReserveStatus.java index f05f18d..ea7879d 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReserveStatus.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReserveStatus.java @@ -1,4 +1,8 @@ package com.yscp.catchtable.domain.reserve.entity.value; public enum ReserveStatus { + RESERVE, + SUCCESS, + DELETE, + CANCEL } diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepository.java b/src/main/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepository.java index ef2b323..17c5909 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepository.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepository.java @@ -8,6 +8,7 @@ import java.time.LocalDate; import java.util.List; +import java.util.Optional; public interface ReserveRepository extends JpaRepository { @@ -40,4 +41,12 @@ public interface ReserveRepository extends JpaRepository { List getStoreReserveDtoBeforeMaxDate(@Param("idx") Long idx, @Param("date") LocalDate date); List findByStore_IdxAndReserveDate(Long idx, LocalDate reserveDate); + + @Query(value = """ + SELECT rd + FROM ReserveData rd + JOIN FETCH rd.store s + WHERE rd.idx = :idx + """) + Optional findWithStoreByIdx(Long idx); } diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java b/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java new file mode 100644 index 0000000..f90aa4d --- /dev/null +++ b/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java @@ -0,0 +1,7 @@ +package com.yscp.catchtable.domain.reserve.repository; + +import com.yscp.catchtable.domain.reserve.entity.UserReserveData; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserReserveDataRepository extends JpaRepository { +} diff --git a/src/main/java/com/yscp/catchtable/domain/user/entity/User.java b/src/main/java/com/yscp/catchtable/domain/user/entity/User.java index 556ca23..e1871a1 100644 --- a/src/main/java/com/yscp/catchtable/domain/user/entity/User.java +++ b/src/main/java/com/yscp/catchtable/domain/user/entity/User.java @@ -5,6 +5,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -31,6 +32,7 @@ public class User { private LocalDateTime modDatetime; + @Builder public User(Long idx, String email, String password, String phone, String nickname, LocalDateTime regDatetime, LocalDateTime modDatetime) { this.idx = idx; this.email = email; diff --git a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java index 8c1e6e7..7aabc93 100644 --- a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java +++ b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java @@ -11,7 +11,14 @@ public enum BadRequestError implements CustomError { /** * 0 ~ 100 Common */ - NULL_EXCEPTION("%s", "1", true); + NULL_EXCEPTION("%s", "1", true), + INVALID_RESERVE_PAY_TYPE("지불 방식이 잘못됐습니다.", "2",false ), + + /** + * 101 ~ 200 예약 + */ + EXPIRED_TICKET("예약 가능 시간을 초과하였습니다. \n 다시 예약 요청을 진행 해주세요.", "101", false), + ; private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST; private final String message; diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java index 6795b85..7f90a3f 100644 --- a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java @@ -2,13 +2,13 @@ import com.yscp.catchtable.application.reserve.ReserveService; import com.yscp.catchtable.application.reserve.ReservesInDayDto; +import com.yscp.catchtable.application.reserve.UserReserveService; +import com.yscp.catchtable.application.reserve.mapper.StoreReserveMapper; +import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; import com.yscp.catchtable.presentation.reserve.dto.response.ReserveInDayResponseDtos; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -16,7 +16,7 @@ @RestController public class ReserveController { - private final ReserveService reserveService; + private final UserReserveService reserveService; @GetMapping("/store/reserves/{storeIdx}") public ResponseEntity getReservesInDay(@PathVariable Long storeIdx, @@ -26,4 +26,12 @@ public ResponseEntity getReservesInDay(@PathVariable L return ResponseEntity.ok(ReserveInDayResponseDtos.from(reservesInDay)); } + + @PostMapping("/store/reserves/{storeIdx}") + public ResponseEntity reserve(@PathVariable Long storeIdx, + @RequestBody StoreReserveRequestDto storeReserveRequestDto) { + StoreReserveMapper.toDto(storeReserveRequestDto); + reserveService.reserve(StoreReserveMapper.toDto(storeReserveRequestDto)); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/dto/StoreReserveRequestDto.java b/src/main/java/com/yscp/catchtable/presentation/reserve/dto/StoreReserveRequestDto.java new file mode 100644 index 0000000..6a891b0 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/dto/StoreReserveRequestDto.java @@ -0,0 +1,11 @@ +package com.yscp.catchtable.presentation.reserve.dto; + +public record StoreReserveRequestDto( + Long userIdx, + Long storeReserveIdx, + String reserveType, + String transactionNo, + String purpose, + Integer reservationNumberOfPeople +) { +} diff --git a/src/test/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepositoryTest.java b/src/test/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepositoryTest.java index 9352320..b880fca 100644 --- a/src/test/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepositoryTest.java +++ b/src/test/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepositoryTest.java @@ -34,7 +34,13 @@ void getStoreReserveDtoBeforeMaxDate() { void findByStore_IdxAndReserveDate() { Assertions.assertThatCode(() -> reserveRepository.findByStore_IdxAndReserveDate(1L, LocalDate.of(2025, 6, 20))) .doesNotThrowAnyException(); + } + @DisplayName("findWithStoreByIdx") + @Test + void findWithStoreByIdx() { + Assertions.assertThatCode(() -> reserveRepository.findWithStoreByIdx(5L)) + .doesNotThrowAnyException(); } } From ca48d3f4a349616fbaa7191fdbc4052f4843deb5 Mon Sep 17 00:00:00 2001 From: yushin Date: Thu, 8 May 2025 15:59:06 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feature/=EC=98=88=EC=95=BD=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=98=88=EC=95=BD=20=EA=B8=B0=EB=8A=A5=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/queue/StoreQueueService.java | 2 +- .../application/reserve/ReserveService.java | 29 ----- .../reserve/UserReserveService.java | 28 +++-- .../reserve/dto/StoreReserveRegisterDto.java | 7 +- .../reserve/mapper/StoreReserveMapper.java | 8 +- .../reserve/mapper/UserReserveDataMapper.java | 8 +- .../domain/reserve/entity/ReserveData.java | 30 +++++ .../catchtable/exception/BadRequestError.java | 4 +- .../reserve/ReserveController.java | 13 +- .../reserve/UserReserveServiceTest.java | 113 ++++++++++++++++++ .../reserve/entity/ReserveDataTest.java | 57 +++++++++ 11 files changed, 242 insertions(+), 57 deletions(-) create mode 100644 src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java create mode 100644 src/test/java/com/yscp/catchtable/domain/reserve/entity/ReserveDataTest.java diff --git a/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java index d2a1f49..c9f83ac 100644 --- a/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java +++ b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java @@ -14,7 +14,7 @@ public void registerWaiting(StoreQueueDto storeQueueDto) { redisTemplate.opsForZSet().add(storeQueueDto.key(), storeQueueDto.value(), storeQueueDto.score()); } - public boolean isValidTicket(StoreQueueDto storeQueueDto) { + public boolean isValidWaitingUser(StoreQueueDto storeQueueDto) { Double score = redisTemplate.opsForZSet().score(storeQueueDto.key(), storeQueueDto.value()); long now = System.currentTimeMillis(); return now - score <= 7 * 60 * 1000; diff --git a/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java b/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java index 48c3187..d8be485 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java @@ -1,14 +1,10 @@ package com.yscp.catchtable.application.reserve; import com.yscp.catchtable.application.queue.StoreQueueService; -import com.yscp.catchtable.application.queue.dto.StoreQueueDto; import com.yscp.catchtable.application.reserve.dto.ReserveDto; import com.yscp.catchtable.application.reserve.dto.StoreReserveDto; -import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; import com.yscp.catchtable.domain.reserve.entity.ReserveData; import com.yscp.catchtable.domain.reserve.repository.ReserveRepository; -import com.yscp.catchtable.exception.BadRequestError; -import com.yscp.catchtable.exception.CatchTableException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -44,31 +40,6 @@ public ReservesInDayDto getReservesInDay(Long storeIdx, LocalDate date) { return ReservesInDayDto.from(reserveDates); } - public void reserve(StoreReserveRegisterDto dto) { - StoreQueueDto storeQueueDto = new StoreQueueDto(dto.storeReserveIdx().toString(), dto.userIdx().toString()); - // 캐시 조회 유효기간 남았는지 - if (ticketValid(storeQueueDto)) { - throw new CatchTableException(BadRequestError.EXPIRED_TICKET); - } - - Optional reserveDataOptional = repository.findWithStoreByIdx(dto.storeReserveIdx()); - reserveDataOptional.ifPresentOrElse( - reserveData -> { - - }, - () -> { - throw new CatchTableException(BadRequestError.NULL_EXCEPTION); - } - ); - // 스토어 및 유저 스토어 조회 - // 유저 예약 데이터 등록 - // 캐쉬 삭제 - } - - private boolean ticketValid(StoreQueueDto queueDto) { - return storeQueueService.isValidTicket(queueDto); - } - public Optional findWithStoreByIdx(Long storeReserveIdx) { return repository.findWithStoreByIdx(storeReserveIdx); } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java index 4f385bb..889bbfd 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java @@ -23,18 +23,22 @@ public class UserReserveService { private final ReserveService reserveService; private final UserReserveDataRepository userReserveDataRepository; - public void reserve(StoreReserveRegisterDto dto) { - StoreQueueDto storeQueueDto = new StoreQueueDto(dto.storeReserveIdx().toString(), dto.userIdx().toString()); + public void reserve(StoreReserveRegisterDto storeReserveRegisterDto) { - if (ticketValid(storeQueueDto)) { + StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserveRegisterDto.storeReserveIdx().toString(), storeReserveRegisterDto.userIdx().toString()); + + if (!isValidWaitingUser(storeQueueDto)) { throw new CatchTableException(BadRequestError.EXPIRED_TICKET); } - Optional reserveDataOptional = reserveService.findWithStoreByIdx(dto.storeReserveIdx()); + Optional reserveDataOptional = reserveService.findWithStoreByIdx(storeReserveRegisterDto.storeReserveIdx()); reserveDataOptional.ifPresentOrElse( reserveData -> { - saveUserReserveData(dto, reserveData, storeQueueDto); + saveUserReserveData( + storeReserveRegisterDto, + reserveData, + storeQueueDto); }, () -> { throw new CatchTableException(BadRequestError.NULL_EXCEPTION); @@ -42,16 +46,22 @@ public void reserve(StoreReserveRegisterDto dto) { ); } - private void saveUserReserveData(StoreReserveRegisterDto dto, ReserveData reserveData, StoreQueueDto storeQueueDto) { + private void saveUserReserveData(StoreReserveRegisterDto dto, + ReserveData reserveData, + StoreQueueDto storeQueueDto) { + + reserveData.userReserve(dto.requestDatetime(), dto.userIdx()); + UserReserveData userReserveData = UserReserveDataMapper.toEntity(reserveData, dto); + if (userReserveData != null) { userReserveDataRepository.save(userReserveData); } - // 티켓 삭제 + storeQueueService.delete(storeQueueDto); } - private boolean ticketValid(StoreQueueDto queueDto) { - return storeQueueService.isValidTicket(queueDto); + private boolean isValidWaitingUser(StoreQueueDto queueDto) { + return storeQueueService.isValidWaitingUser(queueDto); } } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java b/src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java index 235c5a0..fef0ba9 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/dto/StoreReserveRegisterDto.java @@ -1,11 +1,14 @@ package com.yscp.catchtable.application.reserve.dto; +import java.time.LocalDateTime; + public record StoreReserveRegisterDto( Long userIdx, Long storeReserveIdx, - String reserveType, + String reservePayType, String transactionNo, String purpose, - Integer reservationNumberOfPeople + Integer reservationNumberOfPeople, + LocalDateTime requestDatetime ) { } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java b/src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java index f8fea1d..5e23f8d 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/mapper/StoreReserveMapper.java @@ -4,16 +4,20 @@ import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; import lombok.experimental.UtilityClass; +import java.time.LocalDateTime; + @UtilityClass public class StoreReserveMapper { - public static StoreReserveRegisterDto toDto(StoreReserveRequestDto storeReserveRequestDto) { + public static StoreReserveRegisterDto toDto(StoreReserveRequestDto storeReserveRequestDto, + LocalDateTime reserveDatetime) { return new StoreReserveRegisterDto( storeReserveRequestDto.userIdx(), storeReserveRequestDto.storeReserveIdx(), storeReserveRequestDto.reserveType(), storeReserveRequestDto.transactionNo(), storeReserveRequestDto.purpose(), - storeReserveRequestDto.reservationNumberOfPeople() + storeReserveRequestDto.reservationNumberOfPeople(), + reserveDatetime ); } } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java b/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java index 27e148c..adcf408 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java @@ -3,12 +3,10 @@ import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; import com.yscp.catchtable.domain.reserve.entity.ReserveData; import com.yscp.catchtable.domain.reserve.entity.UserReserveData; -import com.yscp.catchtable.domain.reserve.entity.value.ReserveStatus; import com.yscp.catchtable.domain.reserve.entity.value.ReservePayType; +import com.yscp.catchtable.domain.reserve.entity.value.ReserveStatus; import com.yscp.catchtable.domain.user.entity.User; -import java.time.LocalDateTime; - public class UserReserveDataMapper { public static UserReserveData toEntity(ReserveData reserveData, StoreReserveRegisterDto dto) { User requestUser = User.builder() @@ -19,9 +17,9 @@ public static UserReserveData toEntity(ReserveData reserveData, StoreReserveRegi .user(requestUser) .reserveData(reserveData) .reserveStatus(ReserveStatus.RESERVE) - .reserveType(ReservePayType.from(dto.reserveType())) + .reservePayType(ReservePayType.from(dto.reservePayType())) .regIdx(dto.userIdx()) - .regDatetime(LocalDateTime.now()) + .regDatetime(dto.requestDatetime()) .build(); } } diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/ReserveData.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/ReserveData.java index cfabe48..bb52475 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/entity/ReserveData.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/ReserveData.java @@ -2,8 +2,11 @@ import com.yscp.catchtable.domain.reserve.entity.value.StoreReserveDataStatus; import com.yscp.catchtable.domain.store.entity.Store; +import com.yscp.catchtable.exception.BadRequestError; +import com.yscp.catchtable.exception.CatchTableException; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; @@ -44,4 +47,31 @@ public class ReserveData { private Long regIdx; private LocalDateTime modDatetime; private Long modIdx; + + @Builder + public ReserveData(Long idx, Store store, LocalDate reserveDate, String reserveTime, Integer minUserCount, Integer maxUserCount, Integer canReserveCount, Integer reservedCount, StoreReserveDataStatus reserveStatus, LocalDateTime regDatetime, Long regIdx, LocalDateTime modDatetime, Long modIdx) { + this.idx = idx; + this.store = store; + this.reserveDate = reserveDate; + this.reserveTime = reserveTime; + this.minUserCount = minUserCount; + this.maxUserCount = maxUserCount; + this.canReserveCount = canReserveCount; + this.reservedCount = reservedCount; + this.reserveStatus = reserveStatus; + this.regDatetime = regDatetime; + this.regIdx = regIdx; + this.modDatetime = modDatetime; + this.modIdx = modIdx; + } + + public void userReserve(LocalDateTime userReserveDatetime, Long userIdx) { + if (reservedCount >= canReserveCount) { + throw new CatchTableException(BadRequestError.STORE_RESERVATION_MAX); + } + + reservedCount += 1; + modDatetime = userReserveDatetime; + modIdx = userIdx; + } } diff --git a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java index 7aabc93..8dadb16 100644 --- a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java +++ b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java @@ -12,13 +12,13 @@ public enum BadRequestError implements CustomError { * 0 ~ 100 Common */ NULL_EXCEPTION("%s", "1", true), - INVALID_RESERVE_PAY_TYPE("지불 방식이 잘못됐습니다.", "2",false ), + INVALID_RESERVE_PAY_TYPE("지불 방식이 잘못됐습니다.", "2", false), /** * 101 ~ 200 예약 */ EXPIRED_TICKET("예약 가능 시간을 초과하였습니다. \n 다시 예약 요청을 진행 해주세요.", "101", false), - ; + STORE_RESERVATION_MAX("모든 예약이 완료된 상태입니다. \n 다음에 이용해주세요.", "102", false); private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST; private final String message; diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java index 7f90a3f..815015c 100644 --- a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java @@ -11,12 +11,13 @@ import org.springframework.web.bind.annotation.*; import java.time.LocalDate; +import java.time.LocalDateTime; @RequiredArgsConstructor @RestController public class ReserveController { - - private final UserReserveService reserveService; + private final UserReserveService userReserveService; + private final ReserveService reserveService; @GetMapping("/store/reserves/{storeIdx}") public ResponseEntity getReservesInDay(@PathVariable Long storeIdx, @@ -27,11 +28,9 @@ public ResponseEntity getReservesInDay(@PathVariable L return ResponseEntity.ok(ReserveInDayResponseDtos.from(reservesInDay)); } - @PostMapping("/store/reserves/{storeIdx}") - public ResponseEntity reserve(@PathVariable Long storeIdx, - @RequestBody StoreReserveRequestDto storeReserveRequestDto) { - StoreReserveMapper.toDto(storeReserveRequestDto); - reserveService.reserve(StoreReserveMapper.toDto(storeReserveRequestDto)); + @PostMapping("/store/reserves") + public ResponseEntity reserve(@RequestBody StoreReserveRequestDto storeReserveRequestDto) { + userReserveService.reserve(StoreReserveMapper.toDto(storeReserveRequestDto, LocalDateTime.now())); return ResponseEntity.ok().build(); } } diff --git a/src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java b/src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java new file mode 100644 index 0000000..5fc7eba --- /dev/null +++ b/src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java @@ -0,0 +1,113 @@ +package com.yscp.catchtable.application.reserve; + +import com.yscp.catchtable.application.queue.StoreQueueService; +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; +import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; +import com.yscp.catchtable.domain.reserve.entity.ReserveData; +import com.yscp.catchtable.domain.reserve.entity.UserReserveData; +import com.yscp.catchtable.domain.reserve.repository.UserReserveDataRepository; +import com.yscp.catchtable.exception.CatchTableException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; + +@DisplayName("UserReserveService ") +@ExtendWith(MockitoExtension.class) +class UserReserveServiceTest { + @Mock + private StoreQueueService storeQueueService; + @Mock + private ReserveService reserveService; + @Mock + private UserReserveDataRepository userReserveDataRepository; + @InjectMocks + private UserReserveService userReserveService; + + private StoreReserveRegisterDto storeReserveRegisterDto; + + @DisplayName("reserve 메소드는") + @Nested + class Describe_with_reserve { + + @BeforeEach + void setUp() { + storeReserveRegisterDto = new StoreReserveRegisterDto( + 1L, + 1L, + "CARD", + "234123", + "DATE", + 4, + LocalDateTime.now() + ); + } + + @DisplayName("대기열에 존재하는 유저가 요청 했을 경우 ") + @Nested + class Context_with_valid_waitingUser { + + @DisplayName("예약 정보를 저장한다.") + @Test + public void save_user_reserve_data() { + ReserveData mockReserveData = ReserveData.builder() + .idx(1L) + .build(); + + Mockito.when(reserveService.findWithStoreByIdx(any())).thenReturn(Optional.of(mockReserveData)); + + Mockito.when(storeQueueService.isValidWaitingUser(Mockito.any(StoreQueueDto.class))).thenReturn(Boolean.TRUE); + + userReserveService.reserve(storeReserveRegisterDto); + + Mockito.verify(userReserveDataRepository, Mockito.times(1)).save(any(UserReserveData.class)); + Mockito.verify(storeQueueService, Mockito.times(1)).delete(any(StoreQueueDto.class)); + } + } + + + @DisplayName("대기열에 존재하지 않는 유저가 예약 요청을 했을 경우") + @Nested + class Context_with_invalid_waitingUser { + + @DisplayName("에러를 던진다.") + @Test + public void save_user_reserve_data() { + Mockito.when(storeQueueService.isValidWaitingUser(Mockito.any(StoreQueueDto.class))).thenReturn(Boolean.TRUE); + + Mockito.when(reserveService.findWithStoreByIdx(any())).thenReturn(Optional.empty()); + + Assertions.assertThatThrownBy(() -> userReserveService.reserve(storeReserveRegisterDto)) + .isInstanceOf(CatchTableException.class); + } + } + + + @DisplayName("상점 예약 정보가 존재하지 않을 경우") + @Nested + class Context_with_not_found_store_reserve_data { + + @DisplayName("에러를 던진다.") + @Test + public void save_user_reserve_data() { + Mockito.when(storeQueueService.isValidWaitingUser(Mockito.any(StoreQueueDto.class))).thenReturn(Boolean.FALSE); + + Assertions.assertThatThrownBy(() -> userReserveService.reserve(storeReserveRegisterDto)) + .isInstanceOf(CatchTableException.class); + } + } + } + + +} diff --git a/src/test/java/com/yscp/catchtable/domain/reserve/entity/ReserveDataTest.java b/src/test/java/com/yscp/catchtable/domain/reserve/entity/ReserveDataTest.java new file mode 100644 index 0000000..0d60e22 --- /dev/null +++ b/src/test/java/com/yscp/catchtable/domain/reserve/entity/ReserveDataTest.java @@ -0,0 +1,57 @@ +package com.yscp.catchtable.domain.reserve.entity; + +import com.yscp.catchtable.exception.CatchTableException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +class ReserveDataTest { + + @DisplayName("userReserve 메소드는") + @Nested + class Describe_with_userReserve { + + @DisplayName("유저가 예약을 했을 경우") + @Nested + class Context_with_valid_user_reserve { + + @DisplayName("예약 카운트를 증가한다.") + @Test + public void plus_reserve_count() { + ReserveData reserveData = ReserveData.builder() + .idx(1L) + .canReserveCount(5) + .reservedCount(1) + .build(); + + reserveData.userReserve(LocalDateTime.now(), 1L); + + Assertions.assertThat(reserveData.getReservedCount()).isEqualTo(2); + } + } + + @DisplayName("예약 횟수가 가득찼을 경우") + @Nested + class Context_with_max_reserve_count { + + @DisplayName("에러를 던진다.") + @Test + public void plus_reserve_count() { + ReserveData reserveData = ReserveData.builder() + .idx(1L) + .canReserveCount(5) + .reservedCount(5) + .build(); + + Assertions.assertThatThrownBy(() -> reserveData.userReserve(LocalDateTime.now(), 1L)) + .isInstanceOf(CatchTableException.class); + + } + } + } + + +} From dc82912617d6fc85c7a7388aac70e1596f89d250 Mon Sep 17 00:00:00 2001 From: yushin Date: Thu, 8 May 2025 16:12:26 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feature/=EC=98=88=EC=95=BD=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=B6=84=EC=82=B0=20=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +++ .../catchtable/exception/BadRequestError.java | 4 ++- .../reserve/ReserveController.java | 25 ++++++++++++++++++- src/main/resources/application.yml | 5 ++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index b58f065..81baa60 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,10 @@ dependencies { // REDIS CACHE implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-cache' + +// https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter + implementation 'org.redisson:redisson-spring-boot-starter:3.46.0' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java index 8dadb16..1fec6c4 100644 --- a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java +++ b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java @@ -18,7 +18,9 @@ public enum BadRequestError implements CustomError { * 101 ~ 200 예약 */ EXPIRED_TICKET("예약 가능 시간을 초과하였습니다. \n 다시 예약 요청을 진행 해주세요.", "101", false), - STORE_RESERVATION_MAX("모든 예약이 완료된 상태입니다. \n 다음에 이용해주세요.", "102", false); + STORE_RESERVATION_MAX("모든 예약이 완료된 상태입니다. \n 다음에 이용해주세요.", "102", false), + ALREADY_RESERVE("현재 예약 요청을 진행할 수 없습니다. \n 다시 요청해주세요.", "103", false), + ; private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST; private final String message; diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java index 815015c..c929bd1 100644 --- a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java @@ -1,23 +1,30 @@ package com.yscp.catchtable.presentation.reserve; +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; import com.yscp.catchtable.application.reserve.ReserveService; import com.yscp.catchtable.application.reserve.ReservesInDayDto; import com.yscp.catchtable.application.reserve.UserReserveService; import com.yscp.catchtable.application.reserve.mapper.StoreReserveMapper; +import com.yscp.catchtable.exception.BadRequestError; +import com.yscp.catchtable.exception.CatchTableException; import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; import com.yscp.catchtable.presentation.reserve.dto.response.ReserveInDayResponseDtos; import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; @RequiredArgsConstructor @RestController public class ReserveController { private final UserReserveService userReserveService; private final ReserveService reserveService; + private final RedissonClient redissonClient; @GetMapping("/store/reserves/{storeIdx}") public ResponseEntity getReservesInDay(@PathVariable Long storeIdx, @@ -30,7 +37,23 @@ public ResponseEntity getReservesInDay(@PathVariable L @PostMapping("/store/reserves") public ResponseEntity reserve(@RequestBody StoreReserveRequestDto storeReserveRequestDto) { - userReserveService.reserve(StoreReserveMapper.toDto(storeReserveRequestDto, LocalDateTime.now())); + StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserveRequestDto.storeReserveIdx().toString(), storeReserveRequestDto.userIdx().toString()); + RLock lock = redissonClient.getLock(storeQueueDto.key()); + try { + if (lock.tryLock(1, 3, TimeUnit.SECONDS)) { + try { + userReserveService.reserve(StoreReserveMapper.toDto(storeReserveRequestDto, LocalDateTime.now())); + + } finally { + lock.unlock(); + } + } else { + throw new CatchTableException(BadRequestError.ALREADY_RESERVE); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return ResponseEntity.ok().build(); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 09d6eac..aa29df0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,3 +19,8 @@ spring: show-sql: true database-platform: org.hibernate.spatial.dialect.mysql.MySQL56InnoDBSpatialDialect + +redisson: + config: + singleServerConfig: + address: "redis://localhost:6379" From 8c19e822f8981b7196e6043af0304ce44cdd795b Mon Sep 17 00:00:00 2001 From: yushin Date: Mon, 26 May 2025 23:48:27 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feature/=EC=98=88=EC=95=BD=ED=95=98?= =?UTF-8?q?=EA=B8=B0=201=EC=B0=A8=20=EB=A6=AC=EB=B7=B0=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/queue/StoreQueueService.java | 4 +- .../application/queue/dto/StoreQueueDto.java | 5 + .../application/redis/RedisLockService.java | 38 +++++ .../application/reserve/ReserveService.java | 10 +- .../application/reserve/ReservesInDayDto.java | 6 +- .../reserve/UserReserveAggregateService.java | 33 +++++ .../reserve/UserReserveService.java | 17 +-- .../reserve/mapper/UserReserveDataMapper.java | 10 +- .../{ReserveData.java => StoreReserve.java} | 22 ++- ...{UserReserveData.java => UserReserve.java} | 18 ++- .../reserve/entity/value/ReservePayType.java | 8 +- ...ataStatus.java => StoreReserveStatus.java} | 2 +- ...itory.java => StoreReserveRepository.java} | 14 +- .../repository/UserReserveDataRepository.java | 7 - .../repository/UserReserveRepository.java | 13 ++ .../catchtable/exception/ServerError.java | 25 ++++ .../catchtable/infra/utils/PointUtils.java | 19 +++ .../reserve/ReserveController.java | 27 +--- src/main/resources/application.yml | 2 +- .../UserReserveAggregateServiceTest.java | 131 ++++++++++++++++++ .../reserve/UserReserveServiceTest.java | 15 +- ...rveDataTest.java => StoreReserveTest.java} | 6 +- ...t.java => StoreReserveRepositoryTest.java} | 4 +- .../infra/utils/PointUtilsTest.java | 20 +++ 24 files changed, 372 insertions(+), 84 deletions(-) create mode 100644 src/main/java/com/yscp/catchtable/application/redis/RedisLockService.java create mode 100644 src/main/java/com/yscp/catchtable/application/reserve/UserReserveAggregateService.java rename src/main/java/com/yscp/catchtable/domain/reserve/entity/{ReserveData.java => StoreReserve.java} (75%) rename src/main/java/com/yscp/catchtable/domain/reserve/entity/{UserReserveData.java => UserReserve.java} (70%) rename src/main/java/com/yscp/catchtable/domain/reserve/entity/value/{StoreReserveDataStatus.java => StoreReserveStatus.java} (76%) rename src/main/java/com/yscp/catchtable/domain/reserve/repository/{ReserveRepository.java => StoreReserveRepository.java} (79%) delete mode 100644 src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java create mode 100644 src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveRepository.java create mode 100644 src/main/java/com/yscp/catchtable/exception/ServerError.java create mode 100644 src/main/java/com/yscp/catchtable/infra/utils/PointUtils.java create mode 100644 src/test/java/com/yscp/catchtable/application/reserve/UserReserveAggregateServiceTest.java rename src/test/java/com/yscp/catchtable/domain/reserve/entity/{ReserveDataTest.java => StoreReserveTest.java} (90%) rename src/test/java/com/yscp/catchtable/domain/reserve/repository/{ReserveRepositoryTest.java => StoreReserveRepositoryTest.java} (94%) create mode 100644 src/test/java/com/yscp/catchtable/infra/utils/PointUtilsTest.java diff --git a/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java index c9f83ac..a3ec888 100644 --- a/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java +++ b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java @@ -11,7 +11,9 @@ public class StoreQueueService { private final RedisTemplate redisTemplate; public void registerWaiting(StoreQueueDto storeQueueDto) { - redisTemplate.opsForZSet().add(storeQueueDto.key(), storeQueueDto.value(), storeQueueDto.score()); + redisTemplate.opsForZSet().add(storeQueueDto.key(), + storeQueueDto.value(), + storeQueueDto.score()); } public boolean isValidWaitingUser(StoreQueueDto storeQueueDto) { diff --git a/src/main/java/com/yscp/catchtable/application/queue/dto/StoreQueueDto.java b/src/main/java/com/yscp/catchtable/application/queue/dto/StoreQueueDto.java index 84937a1..6f7b405 100644 --- a/src/main/java/com/yscp/catchtable/application/queue/dto/StoreQueueDto.java +++ b/src/main/java/com/yscp/catchtable/application/queue/dto/StoreQueueDto.java @@ -7,6 +7,7 @@ public record StoreQueueDto( String userIdx ) { private static final String WAITING_KEY_FORMAT = "store:%s:waiting:%s"; + private static final String RESERVE_KEY_FORMAT = "store:%s:reserve:%s"; public String key() { return String.format(WAITING_KEY_FORMAT, storeReserveIdx, userIdx); @@ -19,4 +20,8 @@ public String value() { public double score() { return Instant.now().toEpochMilli(); } + + public String reserveKey() { + return String.format(RESERVE_KEY_FORMAT, storeReserveIdx, userIdx); + } } diff --git a/src/main/java/com/yscp/catchtable/application/redis/RedisLockService.java b/src/main/java/com/yscp/catchtable/application/redis/RedisLockService.java new file mode 100644 index 0000000..66cd484 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/redis/RedisLockService.java @@ -0,0 +1,38 @@ +package com.yscp.catchtable.application.redis; + +import com.yscp.catchtable.exception.BadRequestError; +import com.yscp.catchtable.exception.CatchTableException; +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +@Service +public class RedisLockService { + private final RedissonClient redissonClient; + + public void lock(Runnable runnable, + String key, + long waitTime, + long leaseTime, + TimeUnit unit) { + RLock lock = redissonClient.getLock(key); + + try { + if (lock.tryLock(waitTime, leaseTime, unit)) { + try { + runnable.run(); + } finally { + lock.unlock(); + } + } else { + throw new CatchTableException(BadRequestError.ALREADY_RESERVE); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java b/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java index d8be485..3198c48 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java @@ -3,8 +3,8 @@ import com.yscp.catchtable.application.queue.StoreQueueService; import com.yscp.catchtable.application.reserve.dto.ReserveDto; import com.yscp.catchtable.application.reserve.dto.StoreReserveDto; -import com.yscp.catchtable.domain.reserve.entity.ReserveData; -import com.yscp.catchtable.domain.reserve.repository.ReserveRepository; +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; +import com.yscp.catchtable.domain.reserve.repository.StoreReserveRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,7 +19,7 @@ @RequiredArgsConstructor @Service public class ReserveService { - private final ReserveRepository repository; + private final StoreReserveRepository repository; private final StoreQueueService storeQueueService; public Map> findReserveDtoMapByStores(List idxes, LocalDate maxDate) { @@ -36,11 +36,11 @@ public List findReserveDtos(Long idx, LocalDate localDate) { } public ReservesInDayDto getReservesInDay(Long storeIdx, LocalDate date) { - List reserveDates = repository.findByStore_IdxAndReserveDate(storeIdx, date); + List reserveDates = repository.findByStore_IdxAndReserveDate(storeIdx, date); return ReservesInDayDto.from(reserveDates); } - public Optional findWithStoreByIdx(Long storeReserveIdx) { + public Optional findWithStoreByIdx(Long storeReserveIdx) { return repository.findWithStoreByIdx(storeReserveIdx); } } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/ReservesInDayDto.java b/src/main/java/com/yscp/catchtable/application/reserve/ReservesInDayDto.java index 38c04c8..0b4c4d7 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/ReservesInDayDto.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/ReservesInDayDto.java @@ -1,7 +1,7 @@ package com.yscp.catchtable.application.reserve; import com.yscp.catchtable.application.reserve.dto.ReserveInDayDto; -import com.yscp.catchtable.domain.reserve.entity.ReserveData; +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; import org.springframework.util.CollectionUtils; import java.util.ArrayList; @@ -10,7 +10,7 @@ public record ReservesInDayDto( List reserves ) { - public static ReservesInDayDto from(List reserveDates) { + public static ReservesInDayDto from(List reserveDates) { if (CollectionUtils.isEmpty(reserveDates)) { return new ReservesInDayDto(new ArrayList<>()); } @@ -18,7 +18,7 @@ public static ReservesInDayDto from(List reserveDates) { return new ReservesInDayDto(convert(reserveDates)); } - private static List convert(List reserveDates) { + private static List convert(List reserveDates) { return reserveDates.stream() .map(reserveData -> new ReserveInDayDto( reserveData.getIdx(), diff --git a/src/main/java/com/yscp/catchtable/application/reserve/UserReserveAggregateService.java b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveAggregateService.java new file mode 100644 index 0000000..3e6a66a --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveAggregateService.java @@ -0,0 +1,33 @@ +package com.yscp.catchtable.application.reserve; + +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; +import com.yscp.catchtable.application.redis.RedisLockService; +import com.yscp.catchtable.application.reserve.mapper.StoreReserveMapper; +import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +@Service +public class UserReserveAggregateService { + private final UserReserveService userReserveService; + private final RedisLockService redisLockService; + + public void reserve(StoreReserveRequestDto storeReserveRequestDto) { + StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserveRequestDto.storeReserveIdx().toString(), + storeReserveRequestDto.userIdx().toString()); + + redisLockService.lock( + () -> userReserveService.reserve(StoreReserveMapper.toDto(storeReserveRequestDto, LocalDateTime.now())), + storeQueueDto.reserveKey(), + 2L, + 3L, + TimeUnit.SECONDS + ); + + + } +} diff --git a/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java index 889bbfd..1641a71 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/UserReserveService.java @@ -4,9 +4,9 @@ import com.yscp.catchtable.application.queue.dto.StoreQueueDto; import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; import com.yscp.catchtable.application.reserve.mapper.UserReserveDataMapper; -import com.yscp.catchtable.domain.reserve.entity.ReserveData; -import com.yscp.catchtable.domain.reserve.entity.UserReserveData; -import com.yscp.catchtable.domain.reserve.repository.UserReserveDataRepository; +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; +import com.yscp.catchtable.domain.reserve.entity.UserReserve; +import com.yscp.catchtable.domain.reserve.repository.UserReserveRepository; import com.yscp.catchtable.exception.BadRequestError; import com.yscp.catchtable.exception.CatchTableException; import lombok.RequiredArgsConstructor; @@ -21,17 +21,18 @@ public class UserReserveService { private final StoreQueueService storeQueueService; private final ReserveService reserveService; - private final UserReserveDataRepository userReserveDataRepository; + private final UserReserveRepository userReserveDataRepository; public void reserve(StoreReserveRegisterDto storeReserveRegisterDto) { - StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserveRegisterDto.storeReserveIdx().toString(), storeReserveRegisterDto.userIdx().toString()); + StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserveRegisterDto.storeReserveIdx().toString(), + storeReserveRegisterDto.userIdx().toString()); if (!isValidWaitingUser(storeQueueDto)) { throw new CatchTableException(BadRequestError.EXPIRED_TICKET); } - Optional reserveDataOptional = reserveService.findWithStoreByIdx(storeReserveRegisterDto.storeReserveIdx()); + Optional reserveDataOptional = reserveService.findWithStoreByIdx(storeReserveRegisterDto.storeReserveIdx()); reserveDataOptional.ifPresentOrElse( reserveData -> { @@ -47,12 +48,12 @@ public void reserve(StoreReserveRegisterDto storeReserveRegisterDto) { } private void saveUserReserveData(StoreReserveRegisterDto dto, - ReserveData reserveData, + StoreReserve reserveData, StoreQueueDto storeQueueDto) { reserveData.userReserve(dto.requestDatetime(), dto.userIdx()); - UserReserveData userReserveData = UserReserveDataMapper.toEntity(reserveData, dto); + UserReserve userReserveData = UserReserveDataMapper.toEntity(reserveData, dto); if (userReserveData != null) { userReserveDataRepository.save(userReserveData); diff --git a/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java b/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java index adcf408..d930d9c 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/mapper/UserReserveDataMapper.java @@ -1,21 +1,21 @@ package com.yscp.catchtable.application.reserve.mapper; import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; -import com.yscp.catchtable.domain.reserve.entity.ReserveData; -import com.yscp.catchtable.domain.reserve.entity.UserReserveData; +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; +import com.yscp.catchtable.domain.reserve.entity.UserReserve; import com.yscp.catchtable.domain.reserve.entity.value.ReservePayType; import com.yscp.catchtable.domain.reserve.entity.value.ReserveStatus; import com.yscp.catchtable.domain.user.entity.User; public class UserReserveDataMapper { - public static UserReserveData toEntity(ReserveData reserveData, StoreReserveRegisterDto dto) { + public static UserReserve toEntity(StoreReserve reserveData, StoreReserveRegisterDto dto) { User requestUser = User.builder() .idx(dto.userIdx()) .build(); - return UserReserveData.builder() + return UserReserve.builder() .user(requestUser) - .reserveData(reserveData) + .storeReserve(reserveData) .reserveStatus(ReserveStatus.RESERVE) .reservePayType(ReservePayType.from(dto.reservePayType())) .regIdx(dto.userIdx()) diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/ReserveData.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/StoreReserve.java similarity index 75% rename from src/main/java/com/yscp/catchtable/domain/reserve/entity/ReserveData.java rename to src/main/java/com/yscp/catchtable/domain/reserve/entity/StoreReserve.java index bb52475..d8c3b2e 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/entity/ReserveData.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/StoreReserve.java @@ -1,6 +1,6 @@ package com.yscp.catchtable.domain.reserve.entity; -import com.yscp.catchtable.domain.reserve.entity.value.StoreReserveDataStatus; +import com.yscp.catchtable.domain.reserve.entity.value.StoreReserveStatus; import com.yscp.catchtable.domain.store.entity.Store; import com.yscp.catchtable.exception.BadRequestError; import com.yscp.catchtable.exception.CatchTableException; @@ -17,8 +17,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity -@Table(name = "store_reserve_data") -public class ReserveData { +@Table(name = "store_reserve") +public class StoreReserve { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -42,14 +42,26 @@ public class ReserveData { @Comment("예약된 횟수") private Integer reservedCount; @Enumerated(EnumType.STRING) - private StoreReserveDataStatus reserveStatus; + private StoreReserveStatus reserveStatus; private LocalDateTime regDatetime; private Long regIdx; private LocalDateTime modDatetime; private Long modIdx; @Builder - public ReserveData(Long idx, Store store, LocalDate reserveDate, String reserveTime, Integer minUserCount, Integer maxUserCount, Integer canReserveCount, Integer reservedCount, StoreReserveDataStatus reserveStatus, LocalDateTime regDatetime, Long regIdx, LocalDateTime modDatetime, Long modIdx) { + public StoreReserve(Long idx, + Store store, + LocalDate reserveDate, + String reserveTime, + Integer minUserCount, + Integer maxUserCount, + Integer canReserveCount, + Integer reservedCount, + StoreReserveStatus reserveStatus, + LocalDateTime regDatetime, + Long regIdx, + LocalDateTime modDatetime, + Long modIdx) { this.idx = idx; this.store = store; this.reserveDate = reserveDate; diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserveData.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserve.java similarity index 70% rename from src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserveData.java rename to src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserve.java index 77aa170..be846be 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserveData.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/UserReserve.java @@ -14,7 +14,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity -public class UserReserveData { +public class UserReserve { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -24,9 +24,9 @@ public class UserReserveData { @ManyToOne(fetch = FetchType.LAZY) private User user; - @JoinColumn(name = "reserve_data_idx") + @JoinColumn(name = "store_reserve_idx") @ManyToOne(fetch = FetchType.LAZY) - private ReserveData reserveData; + private StoreReserve storeReserve; @Enumerated(EnumType.STRING) private ReserveStatus reserveStatus; @@ -38,10 +38,18 @@ public class UserReserveData { private Long modIdx; @Builder - public UserReserveData(Long idx, User user, ReserveData reserveData, ReserveStatus reserveStatus, ReservePayType reservePayType, LocalDateTime regDatetime, Long regIdx, LocalDateTime modDatetime, Long modIdx) { + public UserReserve(Long idx, + User user, + StoreReserve storeReserve, + ReserveStatus reserveStatus, + ReservePayType reservePayType, + LocalDateTime regDatetime, + Long regIdx, + LocalDateTime modDatetime, + Long modIdx) { this.idx = idx; this.user = user; - this.reserveData = reserveData; + this.storeReserve = storeReserve; this.reserveStatus = reserveStatus; this.reservePayType = reservePayType; this.regDatetime = regDatetime; diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java index ae98d4d..d5af7d8 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/ReservePayType.java @@ -22,6 +22,12 @@ public static ReservePayType from(String reserveTypeString) { if (reserveTypeString == null || reserveTypeString.isEmpty()) { throw new CatchTableException(BadRequestError.INVALID_RESERVE_PAY_TYPE); } - return MAPPING.get(reserveTypeString.toUpperCase()); + ReservePayType reservePayType = MAPPING.get(reserveTypeString.toUpperCase()); + + if (reservePayType == null) { + throw new CatchTableException(BadRequestError.INVALID_RESERVE_PAY_TYPE); + } + + return reservePayType; } } diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/StoreReserveDataStatus.java b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/StoreReserveStatus.java similarity index 76% rename from src/main/java/com/yscp/catchtable/domain/reserve/entity/value/StoreReserveDataStatus.java rename to src/main/java/com/yscp/catchtable/domain/reserve/entity/value/StoreReserveStatus.java index 060e1b4..34a4d91 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/StoreReserveDataStatus.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/entity/value/StoreReserveStatus.java @@ -1,6 +1,6 @@ package com.yscp.catchtable.domain.reserve.entity.value; -public enum StoreReserveDataStatus { +public enum StoreReserveStatus { // 활성화 ACTIVE, // 일시정지 diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepository.java b/src/main/java/com/yscp/catchtable/domain/reserve/repository/StoreReserveRepository.java similarity index 79% rename from src/main/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepository.java rename to src/main/java/com/yscp/catchtable/domain/reserve/repository/StoreReserveRepository.java index 17c5909..fea9b1b 100644 --- a/src/main/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepository.java +++ b/src/main/java/com/yscp/catchtable/domain/reserve/repository/StoreReserveRepository.java @@ -1,7 +1,7 @@ package com.yscp.catchtable.domain.reserve.repository; import com.yscp.catchtable.application.reserve.dto.StoreReserveDto; -import com.yscp.catchtable.domain.reserve.entity.ReserveData; +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,7 +10,7 @@ import java.util.List; import java.util.Optional; -public interface ReserveRepository extends JpaRepository { +public interface StoreReserveRepository extends JpaRepository { @Query( value = """ @@ -18,7 +18,7 @@ public interface ReserveRepository extends JpaRepository { reserve_data AS date, SUM(reserved_count) AS reserve, store_idx - FROM store_reserve_data + FROM store_reserve WHERE store_idx IN :storeIdxes AND reserve_data <= :date GROUP BY store_idx,date @@ -32,7 +32,7 @@ public interface ReserveRepository extends JpaRepository { reserve_data AS date, SUM(reserved_count) AS reserve, store_idx - FROM store_reserve_data + FROM store_reserve WHERE store_idx = :idx AND reserve_data <= :date GROUP BY store_idx,date @@ -40,13 +40,13 @@ public interface ReserveRepository extends JpaRepository { , nativeQuery = true) List getStoreReserveDtoBeforeMaxDate(@Param("idx") Long idx, @Param("date") LocalDate date); - List findByStore_IdxAndReserveDate(Long idx, LocalDate reserveDate); + List findByStore_IdxAndReserveDate(Long idx, LocalDate reserveDate); @Query(value = """ SELECT rd - FROM ReserveData rd + FROM StoreReserve rd JOIN FETCH rd.store s WHERE rd.idx = :idx """) - Optional findWithStoreByIdx(Long idx); + Optional findWithStoreByIdx(Long idx); } diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java b/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java deleted file mode 100644 index f90aa4d..0000000 --- a/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveDataRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.yscp.catchtable.domain.reserve.repository; - -import com.yscp.catchtable.domain.reserve.entity.UserReserveData; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserReserveDataRepository extends JpaRepository { -} diff --git a/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveRepository.java b/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveRepository.java new file mode 100644 index 0000000..e10a643 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/domain/reserve/repository/UserReserveRepository.java @@ -0,0 +1,13 @@ +package com.yscp.catchtable.domain.reserve.repository; + +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; +import com.yscp.catchtable.domain.reserve.entity.UserReserve; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface UserReserveRepository extends JpaRepository { + List findByUser_Idx(Long userIdx); + + List findByStoreReserve(StoreReserve storeReserve); +} diff --git a/src/main/java/com/yscp/catchtable/exception/ServerError.java b/src/main/java/com/yscp/catchtable/exception/ServerError.java new file mode 100644 index 0000000..27c8c19 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/exception/ServerError.java @@ -0,0 +1,25 @@ +package com.yscp.catchtable.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +@Getter +public enum ServerError implements CustomError { + + /** + * 0 ~ 100 Common + */ + SERVER_ERROR("캐치테이블 서버에 에러가 발생했습니다.", "1", true), + ; + private final HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + private final String message; + private final String errorCode; + private final Boolean isCustomMessage; + + @Override + public Boolean isCustomMessage() { + return isCustomMessage; + } +} diff --git a/src/main/java/com/yscp/catchtable/infra/utils/PointUtils.java b/src/main/java/com/yscp/catchtable/infra/utils/PointUtils.java new file mode 100644 index 0000000..c7c3ea6 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/infra/utils/PointUtils.java @@ -0,0 +1,19 @@ +package com.yscp.catchtable.infra.utils; + +import com.yscp.catchtable.exception.CatchTableException; +import com.yscp.catchtable.exception.ServerError; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.io.WKTReader; + +public class PointUtils { + + public static Point convertPoint(Double longitude, Double latitude) { + try { + String pointWKT = String.format("POINT(%s %s)", longitude, latitude); + return (Point) new WKTReader().read(pointWKT); + + } catch (Exception e) { + throw new CatchTableException(ServerError.SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java index c929bd1..cddff3c 100644 --- a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java @@ -1,27 +1,22 @@ package com.yscp.catchtable.presentation.reserve; -import com.yscp.catchtable.application.queue.dto.StoreQueueDto; import com.yscp.catchtable.application.reserve.ReserveService; import com.yscp.catchtable.application.reserve.ReservesInDayDto; +import com.yscp.catchtable.application.reserve.UserReserveAggregateService; import com.yscp.catchtable.application.reserve.UserReserveService; -import com.yscp.catchtable.application.reserve.mapper.StoreReserveMapper; -import com.yscp.catchtable.exception.BadRequestError; -import com.yscp.catchtable.exception.CatchTableException; import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; import com.yscp.catchtable.presentation.reserve.dto.response.ReserveInDayResponseDtos; import lombok.RequiredArgsConstructor; -import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.concurrent.TimeUnit; @RequiredArgsConstructor @RestController public class ReserveController { + private final UserReserveAggregateService userReserveAggregateService; private final UserReserveService userReserveService; private final ReserveService reserveService; private final RedissonClient redissonClient; @@ -37,23 +32,7 @@ public ResponseEntity getReservesInDay(@PathVariable L @PostMapping("/store/reserves") public ResponseEntity reserve(@RequestBody StoreReserveRequestDto storeReserveRequestDto) { - StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserveRequestDto.storeReserveIdx().toString(), storeReserveRequestDto.userIdx().toString()); - RLock lock = redissonClient.getLock(storeQueueDto.key()); - try { - if (lock.tryLock(1, 3, TimeUnit.SECONDS)) { - try { - userReserveService.reserve(StoreReserveMapper.toDto(storeReserveRequestDto, LocalDateTime.now())); - - } finally { - lock.unlock(); - } - } else { - throw new CatchTableException(BadRequestError.ALREADY_RESERVE); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - + userReserveAggregateService.reserve(storeReserveRequestDto); return ResponseEntity.ok().build(); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index aa29df0..cf61287 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,7 +14,7 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: none open-in-view: false show-sql: true diff --git a/src/test/java/com/yscp/catchtable/application/reserve/UserReserveAggregateServiceTest.java b/src/test/java/com/yscp/catchtable/application/reserve/UserReserveAggregateServiceTest.java new file mode 100644 index 0000000..8403915 --- /dev/null +++ b/src/test/java/com/yscp/catchtable/application/reserve/UserReserveAggregateServiceTest.java @@ -0,0 +1,131 @@ +package com.yscp.catchtable.application.reserve; + +import com.yscp.catchtable.application.queue.StoreQueueService; +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; +import com.yscp.catchtable.domain.category.entitry.StoreCategory; +import com.yscp.catchtable.domain.category.entitry.value.StoreCategoryCode; +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; +import com.yscp.catchtable.domain.reserve.entity.UserReserve; +import com.yscp.catchtable.domain.reserve.entity.value.StoreReserveStatus; +import com.yscp.catchtable.domain.reserve.repository.StoreReserveRepository; +import com.yscp.catchtable.domain.reserve.repository.UserReserveRepository; +import com.yscp.catchtable.domain.store.entity.Store; +import com.yscp.catchtable.domain.store.repository.StoreRepository; +import com.yscp.catchtable.infra.utils.PointUtils; +import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@SpringBootTest +class UserReserveAggregateServiceTest { + + @Autowired + private UserReserveAggregateService userReserveAggregateService; + + @Autowired + private StoreQueueService storeQueueService; + + @Autowired + private StoreRepository storeRepository; + + @Autowired + private StoreReserveRepository storeReserveRepository; + + @Autowired + private UserReserveRepository userReserveRepository; + + Store store; + + StoreReserve storeReserve; + + @BeforeEach + void setUp() { + // 상점 등록 + store = Store.builder() + .name("Test 상점") + .holiday(List.of("MON")) + .addressCode("강남1단지") + .category(StoreCategory.builder() + .idx(1L) + .code(StoreCategoryCode.SUSHI) + .build()) + .point(PointUtils.convertPoint(37.5189323, 126.88222735)) + .introduce("테스트 상점입니다.") + .directions("길안내") + .build(); + + storeRepository.save(store); + + storeReserve = StoreReserve.builder() + .reserveDate(LocalDate.now().plusDays(1)) + .reserveTime("1230") + .maxUserCount(10) + .minUserCount(2) + .canReserveCount(10) + .reservedCount(0) + .reserveStatus(StoreReserveStatus.ACTIVE) + .store(store) + .regDatetime(LocalDateTime.now()) + .build(); + + storeReserveRepository.save(storeReserve); + } + + @AfterEach + void afterEach() { + storeRepository.delete(store); + storeReserveRepository.delete(storeReserve); + } + + @DisplayName("동시요청 테스트") + @Test + void name() throws InterruptedException { + // 대기열에 등록 + StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserve.getIdx().toString(), "1"); + storeQueueService.registerWaiting(storeQueueDto); + + StoreReserveRequestDto storeReserveRequestDto = new StoreReserveRequestDto(1L, + storeReserve.getIdx(), + "CATCH_PAY", + "Test", + "데이트", + 4 + ); + + + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + + for (int i = 0; i < threadCount; i++) { + executor.execute(() -> { + try { + userReserveAggregateService.reserve(storeReserveRequestDto); + System.out.println("예약 성공"); + } catch (Exception e) { + System.out.println("예약 실패: " + e.getMessage()); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + + List byUserIdx = userReserveRepository.findByStoreReserve(storeReserve); + Assertions.assertThat(byUserIdx.size()).isEqualTo(1); + } +} diff --git a/src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java b/src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java index 5fc7eba..2640ae1 100644 --- a/src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java +++ b/src/test/java/com/yscp/catchtable/application/reserve/UserReserveServiceTest.java @@ -3,9 +3,9 @@ import com.yscp.catchtable.application.queue.StoreQueueService; import com.yscp.catchtable.application.queue.dto.StoreQueueDto; import com.yscp.catchtable.application.reserve.dto.StoreReserveRegisterDto; -import com.yscp.catchtable.domain.reserve.entity.ReserveData; -import com.yscp.catchtable.domain.reserve.entity.UserReserveData; -import com.yscp.catchtable.domain.reserve.repository.UserReserveDataRepository; +import com.yscp.catchtable.domain.reserve.entity.StoreReserve; +import com.yscp.catchtable.domain.reserve.entity.UserReserve; +import com.yscp.catchtable.domain.reserve.repository.UserReserveRepository; import com.yscp.catchtable.exception.CatchTableException; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -31,7 +31,7 @@ class UserReserveServiceTest { @Mock private ReserveService reserveService; @Mock - private UserReserveDataRepository userReserveDataRepository; + private UserReserveRepository userReserveDataRepository; @InjectMocks private UserReserveService userReserveService; @@ -61,8 +61,11 @@ class Context_with_valid_waitingUser { @DisplayName("예약 정보를 저장한다.") @Test public void save_user_reserve_data() { - ReserveData mockReserveData = ReserveData.builder() + StoreReserve mockReserveData = StoreReserve.builder() .idx(1L) + .reserveTime("1320") + .reservedCount(10) + .canReserveCount(20) .build(); Mockito.when(reserveService.findWithStoreByIdx(any())).thenReturn(Optional.of(mockReserveData)); @@ -71,7 +74,7 @@ public void save_user_reserve_data() { userReserveService.reserve(storeReserveRegisterDto); - Mockito.verify(userReserveDataRepository, Mockito.times(1)).save(any(UserReserveData.class)); + Mockito.verify(userReserveDataRepository, Mockito.times(1)).save(any(UserReserve.class)); Mockito.verify(storeQueueService, Mockito.times(1)).delete(any(StoreQueueDto.class)); } } diff --git a/src/test/java/com/yscp/catchtable/domain/reserve/entity/ReserveDataTest.java b/src/test/java/com/yscp/catchtable/domain/reserve/entity/StoreReserveTest.java similarity index 90% rename from src/test/java/com/yscp/catchtable/domain/reserve/entity/ReserveDataTest.java rename to src/test/java/com/yscp/catchtable/domain/reserve/entity/StoreReserveTest.java index 0d60e22..b1e1cda 100644 --- a/src/test/java/com/yscp/catchtable/domain/reserve/entity/ReserveDataTest.java +++ b/src/test/java/com/yscp/catchtable/domain/reserve/entity/StoreReserveTest.java @@ -8,7 +8,7 @@ import java.time.LocalDateTime; -class ReserveDataTest { +class StoreReserveTest { @DisplayName("userReserve 메소드는") @Nested @@ -21,7 +21,7 @@ class Context_with_valid_user_reserve { @DisplayName("예약 카운트를 증가한다.") @Test public void plus_reserve_count() { - ReserveData reserveData = ReserveData.builder() + StoreReserve reserveData = StoreReserve.builder() .idx(1L) .canReserveCount(5) .reservedCount(1) @@ -40,7 +40,7 @@ class Context_with_max_reserve_count { @DisplayName("에러를 던진다.") @Test public void plus_reserve_count() { - ReserveData reserveData = ReserveData.builder() + StoreReserve reserveData = StoreReserve.builder() .idx(1L) .canReserveCount(5) .reservedCount(5) diff --git a/src/test/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepositoryTest.java b/src/test/java/com/yscp/catchtable/domain/reserve/repository/StoreReserveRepositoryTest.java similarity index 94% rename from src/test/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepositoryTest.java rename to src/test/java/com/yscp/catchtable/domain/reserve/repository/StoreReserveRepositoryTest.java index b880fca..f016774 100644 --- a/src/test/java/com/yscp/catchtable/domain/reserve/repository/ReserveRepositoryTest.java +++ b/src/test/java/com/yscp/catchtable/domain/reserve/repository/StoreReserveRepositoryTest.java @@ -10,10 +10,10 @@ import java.util.List; @SpringBootTest -class ReserveRepositoryTest { +class StoreReserveRepositoryTest { @Autowired - private ReserveRepository reserveRepository; + private StoreReserveRepository reserveRepository; @DisplayName("resultDtos") @Test diff --git a/src/test/java/com/yscp/catchtable/infra/utils/PointUtilsTest.java b/src/test/java/com/yscp/catchtable/infra/utils/PointUtilsTest.java new file mode 100644 index 0000000..bde1c59 --- /dev/null +++ b/src/test/java/com/yscp/catchtable/infra/utils/PointUtilsTest.java @@ -0,0 +1,20 @@ +package com.yscp.catchtable.infra.utils; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Point; + +class PointUtilsTest { + + + @DisplayName("포인트 생성 테스트") + @Test + void createPoint() { + double x = 37.5189323; + double y = 126.88222735; + Point point = PointUtils.convertPoint(x, y); + Assertions.assertThat(point.getX()).isEqualTo(x); + Assertions.assertThat(point.getY()).isEqualTo(y); + } +} From 60158c29dcec81fcd9f4f26b5b83e26654b2a363 Mon Sep 17 00:00:00 2001 From: yushin Date: Tue, 27 May 2025 22:58:12 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feature/=EC=98=88=EC=95=BD=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catchtable/presentation/reserve/ReserveController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java index cddff3c..3aae555 100644 --- a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java @@ -3,11 +3,9 @@ import com.yscp.catchtable.application.reserve.ReserveService; import com.yscp.catchtable.application.reserve.ReservesInDayDto; import com.yscp.catchtable.application.reserve.UserReserveAggregateService; -import com.yscp.catchtable.application.reserve.UserReserveService; import com.yscp.catchtable.presentation.reserve.dto.StoreReserveRequestDto; import com.yscp.catchtable.presentation.reserve.dto.response.ReserveInDayResponseDtos; import lombok.RequiredArgsConstructor; -import org.redisson.api.RedissonClient; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -17,16 +15,12 @@ @RestController public class ReserveController { private final UserReserveAggregateService userReserveAggregateService; - private final UserReserveService userReserveService; private final ReserveService reserveService; - private final RedissonClient redissonClient; @GetMapping("/store/reserves/{storeIdx}") public ResponseEntity getReservesInDay(@PathVariable Long storeIdx, @RequestParam LocalDate date) { - ReservesInDayDto reservesInDay = reserveService.getReservesInDay(storeIdx, date); - return ResponseEntity.ok(ReserveInDayResponseDtos.from(reservesInDay)); }