Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,14 @@ public class StoreQueueService {
public void registerWaiting(StoreQueueDto storeQueueDto) {
redisTemplate.opsForZSet().add(storeQueueDto.key(), storeQueueDto.value(), storeQueueDto.score());
}

public boolean isValidWaitingUser(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());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.yscp.catchtable.application.reserve;

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;
Expand All @@ -11,13 +12,15 @@
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Transactional
@RequiredArgsConstructor
@Service
public class ReserveService {
private final ReserveRepository repository;
private final StoreQueueService storeQueueService;

public Map<Long, List<StoreReserveDto>> findReserveDtoMapByStores(List<Long> idxes, LocalDate maxDate) {
List<StoreReserveDto> storeReserveDtos = repository.findStoreReserveDtoListBeforeMaxDate(idxes, maxDate);
Expand All @@ -36,4 +39,8 @@ public ReservesInDayDto getReservesInDay(Long storeIdx, LocalDate date) {
List<ReserveData> reserveDates = repository.findByStore_IdxAndReserveDate(storeIdx, date);
return ReservesInDayDto.from(reserveDates);
}

public Optional<ReserveData> findWithStoreByIdx(Long storeReserveIdx) {
return repository.findWithStoreByIdx(storeReserveIdx);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 storeReserveRegisterDto) {

StoreQueueDto storeQueueDto = new StoreQueueDto(storeReserveRegisterDto.storeReserveIdx().toString(), storeReserveRegisterDto.userIdx().toString());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예약할 때 더블 클릭하면 어떻게 되어요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 생각에는
Lock이 걸려 있어 동시요청이 처리되지 않아
isValidWaitingUser 메소드에서 Exception을 던질 것 같습니다.

혹시 놓친 부분이 있을까요~?


if (!isValidWaitingUser(storeQueueDto)) {
throw new CatchTableException(BadRequestError.EXPIRED_TICKET);
}

Optional<ReserveData> reserveDataOptional = reserveService.findWithStoreByIdx(storeReserveRegisterDto.storeReserveIdx());

reserveDataOptional.ifPresentOrElse(
reserveData -> {
saveUserReserveData(
storeReserveRegisterDto,
reserveData,
storeQueueDto);
},
() -> {
throw new CatchTableException(BadRequestError.NULL_EXCEPTION);
}
);
}

private void saveUserReserveData(StoreReserveRegisterDto dto,
ReserveData reserveData,
StoreQueueDto storeQueueDto) {

reserveData.userReserve(dto.requestDatetime(), dto.userIdx());

UserReserveData userReserveData = UserReserveDataMapper.toEntity(reserveData, dto);

if (userReserveData != null) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 null이 될 수 있는걸까요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수를 통해 호출되는 부분을 사용한다고 생각하여
null Check를 진행했습니다.
로직상 절대 Null이 안될 것 같아요..

이부분은 고민이 되는데 응답값이 항상 null을 안 줄 경우
null 체크를 무시해도 괜찮을까요!?

userReserveDataRepository.save(userReserveData);
}

storeQueueService.delete(storeQueueDto);
}

private boolean isValidWaitingUser(StoreQueueDto queueDto) {
return storeQueueService.isValidWaitingUser(queueDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.yscp.catchtable.application.reserve.dto;

import java.time.LocalDateTime;

public record StoreReserveRegisterDto(
Long userIdx,
Long storeReserveIdx,
String reservePayType,
String transactionNo,
String purpose,
Integer reservationNumberOfPeople,
LocalDateTime requestDatetime
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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;

import java.time.LocalDateTime;

@UtilityClass
public class StoreReserveMapper {
public static StoreReserveRegisterDto toDto(StoreReserveRequestDto storeReserveRequestDto,
Copy link

@f-lab-k f-lab-k May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UtilityClass를 사용하면 static 함수로 만들어 준다고 하는데 static을 붙일 필요가 있을까요?

그리고 궁금한게 UtilityClass를 꼭 사용해야 하는 이유도 궁금합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestDto에서 생성할까 고민하다가.
Entitiy와 Request의 의존성을 제거하고 생성 단일 책임 있는 클래스를 생성할려고 만들었던 것 같습니다.

보통 레이어간의 변환을 어떻게 하시는지 궁금합니다!

LocalDateTime reserveDatetime) {
return new StoreReserveRegisterDto(
storeReserveRequestDto.userIdx(),
storeReserveRequestDto.storeReserveIdx(),
storeReserveRequestDto.reserveType(),
storeReserveRequestDto.transactionNo(),
storeReserveRequestDto.purpose(),
storeReserveRequestDto.reservationNumberOfPeople(),
reserveDatetime
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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.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) {
User requestUser = User.builder()
.idx(dto.userIdx())
.build();

return UserReserveData.builder()
.user(requestUser)
.reserveData(reserveData)
.reserveStatus(ReserveStatus.RESERVE)
.reservePayType(ReservePayType.from(dto.reservePayType()))
.regIdx(dto.userIdx())
.regDatetime(dto.requestDatetime())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

호....인자가 너무 많네요??
이거 라인별로 정리되도록 하면 좋을 것 같아요

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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도...

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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, ReservePayType> 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());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 결과가 null일 경우에도 INVALID_RESERVE_PAY_TYPE을 던져주는게 좋겠어요~~

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
package com.yscp.catchtable.domain.reserve.entity.value;

public enum ReserveStatus {
RESERVE,
SUCCESS,
DELETE,
CANCEL
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

public interface ReserveRepository extends JpaRepository<ReserveData, Long> {

Expand Down Expand Up @@ -40,4 +41,12 @@ public interface ReserveRepository extends JpaRepository<ReserveData, Long> {
List<StoreReserveDto> getStoreReserveDtoBeforeMaxDate(@Param("idx") Long idx, @Param("date") LocalDate date);

List<ReserveData> findByStore_IdxAndReserveDate(Long idx, LocalDate reserveDate);

@Query(value = """
SELECT rd
FROM ReserveData rd
JOIN FETCH rd.store s
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 Data는 꼭 붙어야 하는 suffix일까요?ㅎㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음.. 다시보니 prefix로 유저와 상점을 구분해놔서 굳이.. 안붙여도 될 것 같습니다!
Reserve라는 키워드를 많이 사용할 것 같아 붙여놓긴 했는데 제거하겠습니다. ㅎㅎ

WHERE rd.idx = :idx
""")
Optional<ReserveData> findWithStoreByIdx(Long idx);
}
Original file line number Diff line number Diff line change
@@ -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<UserReserveData, Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 내려쓰기가 있는게 더 좋을 것 같아요

this.idx = idx;
this.email = email;
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/com/yscp/catchtable/exception/BadRequestError.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ 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),
STORE_RESERVATION_MAX("모든 예약이 완료된 상태입니다. \n 다음에 이용해주세요.", "102", false),
ALREADY_RESERVE("현재 예약 요청을 진행할 수 없습니다. \n 다시 요청해주세요.", "103", false),
;

private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
private final String message;
Expand Down
Loading