Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -1,30 +1,28 @@
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<String, String> redisTemplate;

public void registerWaiting(StoreQueueDto storeQueueDto) {

Boolean result = redisTemplate.opsForZSet().addIfAbsent(storeQueueDto.key(),
redisTemplate.opsForZSet().add(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));
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
Expand Up @@ -6,7 +6,8 @@ public record StoreQueueDto(
String storeReserveIdx,
String userIdx
) {
private static final String WAITING_KEY_FORMAT = "store:%s:waiting:v1:%s";
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);
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
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;
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;

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 StoreReserveRepository 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 @@ -33,7 +36,11 @@ public List<ReserveDto> findReserveDtos(Long idx, LocalDate localDate) {
}

public ReservesInDayDto getReservesInDay(Long storeIdx, LocalDate date) {
List<ReserveData> reserveDates = repository.findByStore_IdxAndReserveDate(storeIdx, date);
List<StoreReserve> reserveDates = repository.findByStore_IdxAndReserveDate(storeIdx, date);
return ReservesInDayDto.from(reserveDates);
}

public Optional<StoreReserve> findWithStoreByIdx(Long storeReserveIdx) {
return repository.findWithStoreByIdx(storeReserveIdx);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,15 +10,15 @@
public record ReservesInDayDto(
List<ReserveInDayDto> reserves
) {
public static ReservesInDayDto from(List<ReserveData> reserveDates) {
public static ReservesInDayDto from(List<StoreReserve> reserveDates) {
if (CollectionUtils.isEmpty(reserveDates)) {
return new ReservesInDayDto(new ArrayList<>());
}

return new ReservesInDayDto(convert(reserveDates));
}

private static List<ReserveInDayDto> convert(List<ReserveData> reserveDates) {
private static List<ReserveInDayDto> convert(List<StoreReserve> reserveDates) {
return reserveDates.stream()
.map(reserveData -> new ReserveInDayDto(
reserveData.getIdx(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);


}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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.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;
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 UserReserveRepository userReserveDataRepository;

public void reserve(StoreReserveRegisterDto storeReserveRegisterDto) {

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

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

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

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

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

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

UserReserve 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.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 UserReserve toEntity(StoreReserve reserveData, StoreReserveRegisterDto dto) {
User requestUser = User.builder()
.idx(dto.userIdx())
.build();

return UserReserve.builder()
.user(requestUser)
.storeReserve(reserveData)
.reserveStatus(ReserveStatus.RESERVE)
.reservePayType(ReservePayType.from(dto.reservePayType()))
.regIdx(dto.userIdx())
.regDatetime(dto.requestDatetime())
.build();
}
}

This file was deleted.

Loading