Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -161,27 +161,52 @@ private Page<ReservationListResponseDto> createReservationListDtoPage(
return page.map(reservation -> {
User user = idToUser.get(reservation.getUserId());
if (user == null) {
throw new ReservationException(ErrorCode.RESOURCE_NOT_FOUND,
"User (userId=" + reservation.getUserId() + ")");
throw new ReservationException(
ErrorCode.RESOURCE_NOT_FOUND,
"User (userId=" + reservation.getUserId() + ")"
);
}

RestaurantSlot slot = idToSlot.get(reservation.getSlotId());
if (slot == null) {
throw new ReservationException(ErrorCode.RESOURCE_NOT_FOUND,
throw new ReservationException(
ErrorCode.RESOURCE_NOT_FOUND,
"RestaurantSlot (slotId=" + reservation.getSlotId() + ")"
);
}

Restaurant restaurant = idToRestaurant.get(slot.getRestaurantId());
if (restaurant == null) {
throw new ReservationException(ErrorCode.RESOURCE_NOT_FOUND,
"Restaurant (restaurantId=" + slot.getRestaurantId() + ")");
throw new ReservationException(
ErrorCode.RESOURCE_NOT_FOUND,
"Restaurant (restaurantId=" + slot.getRestaurantId() + ")"
);
}

return ReservationListResponseDto.of(user, reservation, restaurant);
});
}

@Transactional
public Reservation cancel(String email, Long reservationId) {
User user = userRepository.fetchByEmail(email);

Reservation reservation = reservationRepository.fetchById(reservationId);

LocalDateTime now = LocalDateTime.now();
validateCancelable(user.getUserId(), reservation, now);
reservation.cancel();

DailySlotCapacity capacity = dailySlotCapacityRepository
.findBySlotIdAndDate(reservation.getSlotId(), reservation.getVisitAt().toLocalDate())
.orElseThrow(() -> new ReservationException(ErrorCode.RESERVATION_SLOT_NOT_OPENED));

restoreCapacity(capacity, reservation.getPartySize());
reservationRepository.updateStatus(reservation);

return reservation;
}

private Map<Long, RestaurantSlot> loadSlotMap(Page<Reservation> page) {
List<Long> slotIds = page.getContent().stream()
.map(Reservation::getSlotId)
Expand Down Expand Up @@ -221,4 +246,23 @@ private void decreaseCapacity(DailySlotCapacity capacity, int partySize) {
dailySlotCapacityRepository.updateRemainingCount(capacity);
}

private void validateCancelable(Long userId, Reservation reservation, LocalDateTime now) {
if (!reservation.isOwner(userId)) {
throw new ReservationException(ErrorCode.RESERVATION_FORBIDDEN);
}

if (reservation.isAlreadyCanceled()) {
throw new ReservationException(ErrorCode.RESERVATION_ALREADY_CANCELED);
}

if (!reservation.canCancelAt(now)) {
throw new ReservationException(ErrorCode.RESERVATION_CANCEL_DEADLINE_PASSED);
}
}

private void restoreCapacity(DailySlotCapacity capacity, int partySize) {
capacity.increase(partySize);
dailySlotCapacityRepository.updateRemainingCount(capacity);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ public boolean decrease(int partySize) {
return true;
}

public void increase(int partySize) {
this.remainingCount += partySize;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,25 @@ public Reservation(Long reservationId, Long userId, Long slotId, LocalDateTime v
this.status = status;
}

public boolean isOwner(Long userId) {
return this.userId != null && this.userId.equals(userId);
}

public boolean isAlreadyCanceled() {
return this.status == ReservationStatus.CANCELED;
}

/**
* 취소 가능 기한(방문 24시간 전) 내에 있는지 확인
* @param now 현재 시점
*/
public boolean canCancelAt(LocalDateTime now) {
LocalDateTime cancelDeadline = this.visitAt.minusHours(24);
return now.isBefore(cancelDeadline);
}

public void cancel() {
this.status = ReservationStatus.CANCELED;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;

public interface ReservationRepository {

Expand All @@ -28,5 +29,11 @@ Page<Reservation> findOwnerReservations(
Pageable pageable
);

Optional<Reservation> findById(Long reservationId);

Reservation fetchById(Long reservationId);

void updateStatus(Reservation reservation);

void deleteAll();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public enum ErrorCode {
INVALID_PARTY_SIZE("예약 인원 수가 올바르지 않습니다.", HttpStatus.BAD_REQUEST),
RESERVATION_SLOT_NOT_OPENED("해당 날짜의 예약이 오픈되지 않았습니다.", HttpStatus.BAD_REQUEST),
INVALID_RESERVATION_REQUEST("유효하지 않은 예약 요청입니다.", HttpStatus.BAD_REQUEST),
RESERVATION_ALREADY_CANCELED("이미 취소된 예약입니다.", HttpStatus.BAD_REQUEST),
RESERVATION_CANCEL_DEADLINE_PASSED("예약 취소는 방문 24시간 전까지 가능합니다.", HttpStatus.BAD_REQUEST),

// 401 Unauthorized
UNAUTHORIZED("인증되지 않은 사용자입니다.", HttpStatus.UNAUTHORIZED),
Expand All @@ -24,6 +26,7 @@ public enum ErrorCode {

// 403 Forbidden
ACCESS_DENIED("접근 권한이 없습니다.", HttpStatus.FORBIDDEN),
RESERVATION_FORBIDDEN("본인의 예약만 취소할 수 있습니다.", HttpStatus.FORBIDDEN),

// 404 Not Found
RESOURCE_NOT_FOUND("%s를(을) 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
Expand All @@ -41,4 +44,4 @@ public enum ErrorCode {

private final String message;
private final HttpStatus status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,8 @@ public ReservationEntity(Long userId, Long slotId, LocalDateTime visitAt, Intege
this.note = note;
this.status = status;
}

public void updateStatus(ReservationStatus status) {
this.status = status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -10,6 +11,8 @@
import com.reservation.tablereservationservice.domain.reservation.Reservation;
import com.reservation.tablereservationservice.domain.reservation.ReservationRepository;
import com.reservation.tablereservationservice.domain.reservation.ReservationStatus;
import com.reservation.tablereservationservice.global.exception.ErrorCode;
import com.reservation.tablereservationservice.global.exception.ReservationException;
import com.reservation.tablereservationservice.infrastructure.reservation.entity.ReservationEntity;
import com.reservation.tablereservationservice.infrastructure.reservation.mapper.ReservationMapper;

Expand Down Expand Up @@ -65,6 +68,27 @@ public Page<Reservation> findOwnerReservations(
.map(ReservationMapper.INSTANCE::toDomain);
}

@Override
public Optional<Reservation> findById(Long reservationId) {
return reservationEntityRepository.findById(reservationId)
.map(ReservationMapper.INSTANCE::toDomain);
}

@Override
public Reservation fetchById(Long reservationId) {
return findById(reservationId)
.orElseThrow(() -> new ReservationException(ErrorCode.RESOURCE_NOT_FOUND, "Reservation"));
}

@Override
public void updateStatus(Reservation reservation) {
ReservationEntity entity = reservationEntityRepository.findById(reservation.getReservationId())
.orElseThrow(() -> new ReservationException(ErrorCode.RESOURCE_NOT_FOUND, "Reservation"));

entity.updateStatus(reservation.getStatus());
// save() 호출 없음 -> 변경 감지 UPDATE
}

@Override
public void deleteAll() {
reservationEntityRepository.deleteAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -81,4 +81,13 @@ public ApiResponse<PageResponseDto<ReservationListResponseDto>> findOwnerReserva

}

@CustomerOnly
@PostMapping("/{reservationId}/cancel")
public ApiResponse<ReservationResponseDto> cancel(@PathVariable Long reservationId, @LoginUser CurrentUser user) {
Reservation reservation = reservationService.cancel(user.email(), reservationId);
ReservationResponseDto responseDto = ReservationResponseDto.from(reservation);

return ApiResponse.success("예약 취소 성공", responseDto);
}

}
Loading
Loading