diff --git a/build.gradle b/build.gradle index 4adfc7d..b58f065 100644 --- a/build.gradle +++ b/build.gradle @@ -32,8 +32,10 @@ dependencies { implementation 'org.hibernate.orm:hibernate-spatial:' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // REDIS CACHE implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java new file mode 100644 index 0000000..0ff2f00 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/queue/StoreQueueService.java @@ -0,0 +1,30 @@ +package com.yscp.catchtable.application.queue; + +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; +import com.yscp.catchtable.exception.BadRequestError; +import com.yscp.catchtable.exception.CatchTableException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@RequiredArgsConstructor +@Service +public class StoreQueueService { + private final RedisTemplate redisTemplate; + + public void registerWaiting(StoreQueueDto storeQueueDto) { + + Boolean result = redisTemplate.opsForZSet().addIfAbsent(storeQueueDto.key(), + storeQueueDto.value(), + storeQueueDto.score()); + + if (Boolean.FALSE.equals(result)) { + throw new CatchTableException(BadRequestError.ALREADY_REGISTER_WAITING); + } + + redisTemplate.expire(storeQueueDto.key(), Duration.ofMinutes(7L)); + + } +} 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 new file mode 100644 index 0000000..ef8f999 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/queue/dto/StoreQueueDto.java @@ -0,0 +1,22 @@ +package com.yscp.catchtable.application.queue.dto; + +import java.time.Instant; + +public record StoreQueueDto( + String storeReserveIdx, + String userIdx +) { + private static final String WAITING_KEY_FORMAT = "store:%s:waiting:v1:%s"; + + public String key() { + return String.format(WAITING_KEY_FORMAT, storeReserveIdx, userIdx); + } + + public String value() { + return userIdx; + } + + public double score() { + return Instant.now().toEpochMilli(); + } +} 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 bbfbc5d..9b76fb0 100644 --- a/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java +++ b/src/main/java/com/yscp/catchtable/application/reserve/ReserveService.java @@ -2,6 +2,7 @@ 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 lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -30,4 +31,9 @@ public List findReserveDtos(Long idx, LocalDate localDate) { .map(ReserveDto::from) .toList(); } + + public ReservesInDayDto getReservesInDay(Long storeIdx, LocalDate date) { + List reserveDates = repository.findByStore_IdxAndReserveDate(storeIdx, date); + return ReservesInDayDto.from(reserveDates); + } } diff --git a/src/main/java/com/yscp/catchtable/application/reserve/ReservesInDayDto.java b/src/main/java/com/yscp/catchtable/application/reserve/ReservesInDayDto.java new file mode 100644 index 0000000..38c04c8 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/reserve/ReservesInDayDto.java @@ -0,0 +1,29 @@ +package com.yscp.catchtable.application.reserve; + +import com.yscp.catchtable.application.reserve.dto.ReserveInDayDto; +import com.yscp.catchtable.domain.reserve.entity.ReserveData; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +public record ReservesInDayDto( + List reserves +) { + public static ReservesInDayDto from(List reserveDates) { + if (CollectionUtils.isEmpty(reserveDates)) { + return new ReservesInDayDto(new ArrayList<>()); + } + + return new ReservesInDayDto(convert(reserveDates)); + } + + private static List convert(List reserveDates) { + return reserveDates.stream() + .map(reserveData -> new ReserveInDayDto( + reserveData.getIdx(), + reserveData.getReserveTime(), + reserveData.getMaxUserCount() + )).toList(); + } +} diff --git a/src/main/java/com/yscp/catchtable/application/reserve/dto/ReserveInDayDto.java b/src/main/java/com/yscp/catchtable/application/reserve/dto/ReserveInDayDto.java new file mode 100644 index 0000000..9f68f8d --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/reserve/dto/ReserveInDayDto.java @@ -0,0 +1,8 @@ +package com.yscp.catchtable.application.reserve.dto; + +public record ReserveInDayDto( + Long idx, + String reserveTime, + Integer maxUserCount +) { +} diff --git a/src/main/java/com/yscp/catchtable/application/store/mapper/StoreQueueMapper.java b/src/main/java/com/yscp/catchtable/application/store/mapper/StoreQueueMapper.java new file mode 100644 index 0000000..8b98938 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/application/store/mapper/StoreQueueMapper.java @@ -0,0 +1,15 @@ +package com.yscp.catchtable.application.store.mapper; + +import com.yscp.catchtable.application.queue.dto.StoreQueueDto; +import com.yscp.catchtable.presentation.store.dto.StoreQueueRequestDto; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class StoreQueueMapper { + public static StoreQueueDto toQueueDto(StoreQueueRequestDto storeQueueRequestDto) { + return new StoreQueueDto( + storeQueueRequestDto.storeReserveIdx().toString(), + storeQueueRequestDto.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/ReserveData.java index 1362c14..cfabe48 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 @@ -27,9 +27,9 @@ public class ReserveData { private Store store; @Comment("예약 일자") - private LocalDate reserveData; + private LocalDate reserveDate; @Comment("예약 시간") - private Integer reserveTime; + private String reserveTime; @Comment("최소 인원") private Integer minUserCount; @Comment("최대 인원") 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 5744595..ef2b323 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 @@ -38,4 +38,6 @@ public interface ReserveRepository extends JpaRepository { """ , nativeQuery = true) List getStoreReserveDtoBeforeMaxDate(@Param("idx") Long idx, @Param("date") LocalDate date); + + List findByStore_IdxAndReserveDate(Long idx, LocalDate reserveDate); } diff --git a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java index f8b5806..faf3101 100644 --- a/src/main/java/com/yscp/catchtable/exception/BadRequestError.java +++ b/src/main/java/com/yscp/catchtable/exception/BadRequestError.java @@ -11,11 +11,16 @@ public enum BadRequestError implements CustomError { /** * 0 ~ 100 Common */ - NULL_EXCEPTION("%s", 1, true); + NULL_EXCEPTION("%s", "1", true), + + /** + * 200 ~ 300 Waiting + */ + ALREADY_REGISTER_WAITING("이미 예약을 진행하고 있습니다.", "200" , false); private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST; private final String message; - private final Integer errorCode; + private final String errorCode; private final Boolean isCustomMessage; @Override diff --git a/src/main/java/com/yscp/catchtable/exception/CustomError.java b/src/main/java/com/yscp/catchtable/exception/CustomError.java index 676fc41..4423967 100644 --- a/src/main/java/com/yscp/catchtable/exception/CustomError.java +++ b/src/main/java/com/yscp/catchtable/exception/CustomError.java @@ -3,13 +3,10 @@ import org.springframework.http.HttpStatus; public interface CustomError { - HttpStatus getHttpStatus(); - - String getErrorCode(); - - String getMessage(); - - default Boolean isCustomMessage() { + HttpStatus getHttpStatus(); + String getErrorCode(); + String getMessage(); + default Boolean isCustomMessage() { return false; } } diff --git a/src/main/java/com/yscp/catchtable/exception/NotFoundError.java b/src/main/java/com/yscp/catchtable/exception/NotFoundError.java index abb77df..6409376 100644 --- a/src/main/java/com/yscp/catchtable/exception/NotFoundError.java +++ b/src/main/java/com/yscp/catchtable/exception/NotFoundError.java @@ -4,15 +4,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -@RequiredArgsConstructor @Getter +@RequiredArgsConstructor public enum NotFoundError implements CustomError { - NOT_FOUND_STORE("상점을 찾을 수 없습니다.", 100, false), - NOT_FOUND_BUSINESS_HOUR("영업 시간을 조회할 수 없습니다.", 101, false); + NOT_FOUND_STORE("상점을 찾을 수 없습니다.", "100", false), + NOT_FOUND_BUSINESS_HOUR("영업 시간을 조회할 수 없습니다.", "101", false); private final HttpStatus httpStatus = HttpStatus.NOT_FOUND; private final String message; - private final Integer errorCode; + private final String errorCode; private final Boolean isCustomMessage; @Override diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java new file mode 100644 index 0000000..6795b85 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/ReserveController.java @@ -0,0 +1,29 @@ +package com.yscp.catchtable.presentation.reserve; + +import com.yscp.catchtable.application.reserve.ReserveService; +import com.yscp.catchtable.application.reserve.ReservesInDayDto; +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 java.time.LocalDate; + +@RequiredArgsConstructor +@RestController +public class ReserveController { + + private final ReserveService reserveService; + + @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)); + } +} diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/dto/response/ReserveInDayResponseDto.java b/src/main/java/com/yscp/catchtable/presentation/reserve/dto/response/ReserveInDayResponseDto.java new file mode 100644 index 0000000..fe31364 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/dto/response/ReserveInDayResponseDto.java @@ -0,0 +1,8 @@ +package com.yscp.catchtable.presentation.reserve.dto.response; + +public record ReserveInDayResponseDto( + Long idx, + String reserveTime, + Integer maxUserCount +) { +} diff --git a/src/main/java/com/yscp/catchtable/presentation/reserve/dto/response/ReserveInDayResponseDtos.java b/src/main/java/com/yscp/catchtable/presentation/reserve/dto/response/ReserveInDayResponseDtos.java new file mode 100644 index 0000000..65c5dc6 --- /dev/null +++ b/src/main/java/com/yscp/catchtable/presentation/reserve/dto/response/ReserveInDayResponseDtos.java @@ -0,0 +1,28 @@ +package com.yscp.catchtable.presentation.reserve.dto.response; + +import com.yscp.catchtable.application.reserve.ReservesInDayDto; +import com.yscp.catchtable.application.reserve.dto.ReserveInDayDto; + +import java.util.ArrayList; +import java.util.List; + +public record ReserveInDayResponseDtos( + List reserves +) { + public static ReserveInDayResponseDtos from(ReservesInDayDto reservesInDay) { + if (reservesInDay == null) { + return new ReserveInDayResponseDtos(new ArrayList<>()); + } + + return new ReserveInDayResponseDtos(convert(reservesInDay.reserves())); + } + + private static List convert(List reservesInDay) { + return reservesInDay.stream() + .map(reserveInDayDto -> new ReserveInDayResponseDto( + reserveInDayDto.idx(), + reserveInDayDto.reserveTime(), + reserveInDayDto.maxUserCount() + )).toList(); + } +} diff --git a/src/main/java/com/yscp/catchtable/presentation/store/StoreQueueController.java b/src/main/java/com/yscp/catchtable/presentation/store/StoreQueueController.java new file mode 100644 index 0000000..361228e --- /dev/null +++ b/src/main/java/com/yscp/catchtable/presentation/store/StoreQueueController.java @@ -0,0 +1,22 @@ +package com.yscp.catchtable.presentation.store; + +import com.yscp.catchtable.application.queue.StoreQueueService; +import com.yscp.catchtable.application.store.mapper.StoreQueueMapper; +import com.yscp.catchtable.presentation.store.dto.StoreQueueRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class StoreQueueController { + private final StoreQueueService storeQueueService; + + @PostMapping("/store/reservation/enter") + public ResponseEntity enter(@RequestBody StoreQueueRequestDto storeQueueRequestDto) { + storeQueueService.registerWaiting(StoreQueueMapper.toQueueDto(storeQueueRequestDto)); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/yscp/catchtable/presentation/store/dto/StoreQueueRequestDto.java b/src/main/java/com/yscp/catchtable/presentation/store/dto/StoreQueueRequestDto.java new file mode 100644 index 0000000..2bfb5fc --- /dev/null +++ b/src/main/java/com/yscp/catchtable/presentation/store/dto/StoreQueueRequestDto.java @@ -0,0 +1,7 @@ +package com.yscp.catchtable.presentation.store.dto; + +public record StoreQueueRequestDto( + Long storeReserveIdx, + String userIdx +) { +} 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 42ab3f8..9352320 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 @@ -27,6 +27,14 @@ void findStoreReserveDtoListBeforeMaxDate() { void getStoreReserveDtoBeforeMaxDate() { Assertions.assertThatCode(() -> reserveRepository.getStoreReserveDtoBeforeMaxDate(1L, LocalDate.of(2025, 6, 20))) .doesNotThrowAnyException(); + } + + @DisplayName("findByStore_IdxAndReserveDate") + @Test + void findByStore_IdxAndReserveDate() { + Assertions.assertThatCode(() -> reserveRepository.findByStore_IdxAndReserveDate(1L, LocalDate.of(2025, 6, 20))) + .doesNotThrowAnyException(); } } +