diff --git a/src/main/java/com/eatsfine/eatsfine/domain/booking/repository/BookingRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/booking/repository/BookingRepository.java index a8f8e45c..ce500e71 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/booking/repository/BookingRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/booking/repository/BookingRepository.java @@ -102,4 +102,16 @@ Optional findLastConflictingDate( @Param("breakEnd") LocalTime breakEnd, @Param("adjustedBreakStart") LocalTime adjustedBreakStart ); + + @Query("SELECT DISTINCT bt.storeTable.id FROM BookingTable bt " + + "JOIN bt.booking b " + + "WHERE bt.storeTable.id IN :tableIds " + + "AND (b.bookingDate > :currentDate " + + " OR (b.bookingDate = :currentDate AND b.bookingTime >= :currentTime)) " + + "AND b.status IN ('CONFIRMED', 'PENDING')") + List findTableIdsWithFutureBookings( + @Param("tableIds") List tableIds, + @Param("currentDate") LocalDate currentDate, + @Param("currentTime") LocalTime currentTime + ); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java index 2e0262d7..d95f3e9f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java @@ -18,6 +18,7 @@ public interface TableLayoutControllerDocs { - 그리드 크기는 1x1 ~ 10x10 범위 내에서 설정 가능합니다. - 가게당 활성 배치도는 1개만 존재하며, 새 배치도 생성 시 기존 배치도는 자동으로 비활성화됩니다. + - 기존 배치도에 존재하는 테이블에 예약이 있다면, 배치도 생성 시 LAYOUT400_2 에러메세지를 반환합니다. - 생성된 배치도는 빈 상태로 생성되며, 이후 테이블 추가 API를 통해 테이블을 배치할 수 있습니다. """ ) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java index 14b6fa17..6cffad8b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java @@ -19,7 +19,10 @@ @Builder @SQLDelete(sql = "UPDATE table_layout SET is_deleted = true, is_active = false, deleted_at = CURRENT_TIMESTAMP WHERE id = ?") @SQLRestriction("is_deleted = false") -@Table(name = "table_layout") +@Table( + name = "table_layout", + uniqueConstraints = {@UniqueConstraint(name = "uk_table_layout_store_active", columnNames = {"store_active_key"})} +) public class TableLayout extends BaseEntity { @Id @@ -39,6 +42,16 @@ public class TableLayout extends BaseEntity { @Column(name = "is_active", nullable = false) private boolean isActive; + // 가상 컬럼 추가 (JPA에서는 직접 사용하지 않지만 DB 레벨에서 동작) + // is_active = true일 때만 store_id 값을 가지고, false일 때는 NULL + @Column( + name = "store_active_key", + columnDefinition = "VARCHAR(50) GENERATED ALWAYS AS (CASE WHEN is_active = true THEN CONCAT('store_', store_id) ELSE NULL END) STORED", + insertable = false, + updatable = false + ) + private String storeActiveKey; + @Column(name = "is_deleted", nullable = false) @Builder.Default private boolean isDeleted = false; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/exception/status/TableLayoutErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/exception/status/TableLayoutErrorStatus.java index c0d02e4c..a532af0f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/exception/status/TableLayoutErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/exception/status/TableLayoutErrorStatus.java @@ -13,6 +13,8 @@ public enum TableLayoutErrorStatus implements BaseErrorCode { _LAYOUT_NOT_FOUND(HttpStatus.NOT_FOUND, "LAYOUT404", "배치도를 찾을 수 없습니다."), _LAYOUT_FORBIDDEN(HttpStatus.FORBIDDEN, "LAYOUT403", "해당 가게의 소유자만 접근 가능합니다."), + + _CANNOT_DELETE_LAYOUT_WITH_FUTURE_BOOKINGS(HttpStatus.BAD_REQUEST, "LAYOUT400_2", "미래 예약이 있는 배치도는 삭제할 수 없습니다. 모든 예약이 완료된 후 재생성해주세요.") ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java index ced9c523..dbba1ca1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java @@ -1,25 +1,31 @@ package com.eatsfine.eatsfine.domain.table_layout.service; +import com.eatsfine.eatsfine.domain.booking.repository.BookingRepository; import com.eatsfine.eatsfine.domain.store.entity.Store; -import com.eatsfine.eatsfine.domain.store.exception.StoreException; -import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; -import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; +import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; import com.eatsfine.eatsfine.domain.table_layout.converter.TableLayoutConverter; import com.eatsfine.eatsfine.domain.table_layout.dto.req.TableLayoutReqDto; import com.eatsfine.eatsfine.domain.table_layout.dto.res.TableLayoutResDto; import com.eatsfine.eatsfine.domain.table_layout.entity.TableLayout; +import com.eatsfine.eatsfine.domain.table_layout.exception.TableLayoutException; +import com.eatsfine.eatsfine.domain.table_layout.exception.status.TableLayoutErrorStatus; import com.eatsfine.eatsfine.domain.table_layout.repository.TableLayoutRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + @Service @RequiredArgsConstructor @Transactional public class TableLayoutCommandServiceImpl implements TableLayoutCommandService { - private final StoreRepository storeRepository; private final TableLayoutRepository tableLayoutRepository; + private final BookingRepository bookingRepository; private final StoreValidator storeValidator; // 테이블 배치도 생성 @@ -32,7 +38,19 @@ public TableLayoutResDto.LayoutDetailDto createLayout( Store store = storeValidator.validateStoreOwner(storeId, email); - deactivateExistingLayout(store); + Optional existingLayout = tableLayoutRepository.findByStoreIdAndIsActiveTrue(storeId); + + if (existingLayout.isPresent()) { + // 미래 예약 확인 + boolean hasFutureBookings = checkFutureBookingsInLayout(existingLayout.get()); + + if (hasFutureBookings) { + throw new TableLayoutException(TableLayoutErrorStatus._CANNOT_DELETE_LAYOUT_WITH_FUTURE_BOOKINGS); + } + + // 미래 예약이 없으면 배치도 비활성화 후 재생성 + tableLayoutRepository.delete(existingLayout.get()); + } // 새 배치도 생성 TableLayout newLayout = TableLayout.builder() @@ -48,9 +66,20 @@ public TableLayoutResDto.LayoutDetailDto createLayout( return TableLayoutConverter.toLayoutDetailDto(savedLayout); } - // 기존 테이블 배치도 비활성화 - private void deactivateExistingLayout(Store store) { - tableLayoutRepository.findByStoreIdAndIsActiveTrue(store.getId()) - .ifPresent(tableLayoutRepository::delete); + // 미래 예약 확인 + private boolean checkFutureBookingsInLayout(TableLayout layout) { + LocalDate currentDate = LocalDate.now(); + LocalTime currentTime = LocalTime.now(); + List tableIds = layout.getTables().stream() + .map(StoreTable::getId) + .toList(); + + if (tableIds.isEmpty()) { + return false; + } + + List tableIdsWithFutureBookings = bookingRepository.findTableIdsWithFutureBookings(tableIds, currentDate, currentTime); + + return !tableIdsWithFutureBookings.isEmpty(); } }