Skip to content

Commit

Permalink
Merge pull request #12 from f-lab-edu/feat/#8-develop-booking
Browse files Browse the repository at this point in the history
[Feat] 예약 현황 조회 / 예약 하기 기능 구현
  • Loading branch information
uijin31 authored Dec 10, 2024
2 parents 6a06bd2 + ff77318 commit 591fe0b
Show file tree
Hide file tree
Showing 22 changed files with 720 additions and 40 deletions.
4 changes: 4 additions & 0 deletions nowait-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// H2
runtimeOnly 'com.h2database:h2'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.nowait.booking.api;

import com.nowait.booking.application.BookingService;
import com.nowait.booking.dto.TimeSlotDto;
import com.nowait.booking.dto.request.BookingReq;
import com.nowait.booking.dto.response.BookingRes;
Expand Down Expand Up @@ -28,6 +29,8 @@
@RequiredArgsConstructor
public class BookingApi {

private final BookingService bookingService;

/**
* 가게 예약 현황 조회 API
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.nowait.booking.application;

import com.nowait.booking.domain.model.Booking;
import com.nowait.common.event.BookedEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class BookingEventPublisher {

private final ApplicationEventPublisher eventPublisher;

@Async
public void publishBookedEvent(Booking booking, Long placeId) {
eventPublisher.publishEvent(
new BookedEvent(booking.getId(), placeId, booking.getUserId()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.nowait.booking.application;

import com.nowait.booking.domain.model.Booking;
import com.nowait.booking.domain.model.BookingSlot;
import com.nowait.booking.domain.repository.BookingRepository;
import com.nowait.booking.domain.repository.BookingSlotRepository;
import com.nowait.booking.dto.TimeSlotDto;
import com.nowait.booking.dto.response.BookingRes;
import com.nowait.booking.dto.response.DailyBookingStatusRes;
import com.nowait.place.domain.repository.PlaceRepository;
import com.nowait.user.domain.repository.UserRepository;
import jakarta.persistence.EntityNotFoundException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookingService {

private final BookingSlotRepository bookingSlotRepository;
private final BookingRepository bookingRepository;
private final UserRepository userRepository;
private final PlaceRepository placeRepository;
private final BookingEventPublisher bookingEventPublisher;

public DailyBookingStatusRes getDailyBookingStatus(Long placeId, LocalDate date) {
List<BookingSlot> bookingSlots = bookingSlotRepository.findAllByPlaceIdAndDate(
placeId, date);

List<TimeSlotDto> timeSlots = bookingSlots.stream()
.collect(Collectors.groupingBy(BookingSlot::getTime))
.entrySet().stream()
.map(entry -> new TimeSlotDto(entry.getKey(), isAvailable(entry.getValue())))
.toList();

return new DailyBookingStatusRes(placeId, date, timeSlots);
}

@Transactional
public BookingRes book(Long loginId, Long placeId, LocalDate date, LocalTime time,
Integer partySize) {
validateUserExist(loginId, "존재하지 않는 사용자의 요청입니다.");
validatePlaceExist(placeId, "존재하지 않는 식당입니다.");

BookingSlot slot = findAvailableSlot(placeId, date, time);
Booking booking = bookingRepository.save(Booking.of(loginId, partySize, slot));

bookingEventPublisher.publishBookedEvent(booking, placeId);

return BookingRes.of(booking, slot);
}

private boolean isAvailable(List<BookingSlot> slots) {
// 모든 슬롯이 예약된 경우에만 false 반환
return slots.stream().anyMatch(slot -> !slot.isBooked());
}

private void validateUserExist(Long userId, String errorMessage) {
if (!userRepository.existsById(userId)) {
throw new EntityNotFoundException(errorMessage);
}
}

private void validatePlaceExist(Long placeId, String errorMessage) {
if (!placeRepository.existsById(placeId)) {
throw new EntityNotFoundException(errorMessage);
}
}

private BookingSlot findAvailableSlot(Long placeId, LocalDate date, LocalTime time) {
return bookingSlotRepository.findFirstByPlaceIdAndDateAndTimeAndIsBookedFalse(placeId, date,
time).orElseThrow(() -> new IllegalArgumentException("예약 가능한 테이블이 없습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.nowait.booking.domain.model;

import com.nowait.common.domain.model.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "booking")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Booking extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Column(name = "booking_slot_id", nullable = false, unique = true)
private Long bookingSlotId;

@Column(name = "user_id", nullable = false)
private Long userId;

@Enumerated(value = EnumType.STRING)
@Column(name = "status", nullable = false, columnDefinition = "varchar(20)")
private BookingStatus status;

@Column(name = "party_size")
private Integer partySize;

public static Booking of(Long userId, Integer partySize, BookingSlot slot) {
return new Booking(
null,
slot.getId(),
userId,
slot.book(),
partySize
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.nowait.booking.domain.model;

import com.nowait.common.domain.model.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import java.time.LocalTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "booking_slot")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BookingSlot extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Column(name = "place_id", nullable = false)
private Long placeId;

@Column(name = "table_id", nullable = false)
private Long tableId;

@Column(name = "date", nullable = false)
private LocalDate date;

@Column(name = "time", nullable = false)
private LocalTime time;

@Column(name = "is_booked")
private boolean isBooked;

@Column(name = "deposit_required")
private boolean depositRequired;

@Column(name = "confirm_required")
private boolean confirmRequired;

@Column(name = "deposit_policy_id")
private Long deposit_policy_id;

public BookingStatus book() {
if (isBooked) {
throw new IllegalArgumentException("이미 예약된 테이블입니다.");
}

isBooked = true;

return isDepositRequired() ? BookingStatus.PENDING_PAYMENT
: isConfirmRequired() ? BookingStatus.PENDING_CONFIRM : BookingStatus.CONFIRMED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.nowait.booking.domain.model;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum BookingStatus {
CONFIRMED("확정됨"),
PENDING_CONFIRM("확정 대기 중"),
PENDING_PAYMENT("결재 대기 중"),
CANCELLED("취소됨");

private final String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.nowait.booking.domain.repository;

import com.nowait.booking.domain.model.Booking;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookingRepository extends JpaRepository<Booking, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.nowait.booking.domain.repository;

import com.nowait.booking.domain.model.BookingSlot;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookingSlotRepository extends JpaRepository<BookingSlot, Long> {

List<BookingSlot> findAllByPlaceIdAndDate(Long placeId, LocalDate date);

Optional<BookingSlot> findFirstByPlaceIdAndDateAndTimeAndIsBookedFalse(Long placeId,
LocalDate date, LocalTime time);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
package com.nowait.booking.dto.response;

import com.nowait.booking.domain.model.Booking;
import com.nowait.booking.domain.model.BookingSlot;

public record BookingRes(
Long bookingId,
String bookingStatus,
boolean depositRequired,
boolean confirmRequired
) {

public static BookingRes of(Booking booking, BookingSlot slot) {
return new BookingRes(
booking.getId(),
booking.getStatus().getDescription(),
slot.isDepositRequired(),
slot.isConfirmRequired()
);
}
}
10 changes: 10 additions & 0 deletions nowait-api/src/main/java/com/nowait/common/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.nowait.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.nowait.common.domain.model;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nowait.common.event;

public record BookedEvent(
Long bookingId,
Long placeId,
Long userId
) {

}
Loading

0 comments on commit 591fe0b

Please sign in to comment.