From 2df52a8f1da0d2955b87441afcc5d648a3f48781 Mon Sep 17 00:00:00 2001 From: sonjunkyu Date: Thu, 12 Feb 2026 00:49:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT]:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=EB=8F=84=20Unique=20=EC=A0=9C=EC=95=BD=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4,=20=EA=B0=80=EC=83=81=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/table_layout/entity/TableLayout.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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; From da82fbe67d233650dae5ea244bbee051c3233063 Mon Sep 17 00:00:00 2001 From: sonjunkyu Date: Thu, 12 Feb 2026 00:52:01 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[REFACTOR]:=20=EB=B0=B0=EC=B9=98=EB=8F=84?= =?UTF-8?q?=20=EC=9E=AC=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EB=AF=B8=EB=9E=98?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TableLayoutControllerDocs.java | 1 + .../status/TableLayoutErrorStatus.java | 2 + .../TableLayoutCommandServiceImpl.java | 37 ++++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) 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/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..f7d0e7a1 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,29 @@ 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.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.status.TableLayoutErrorStatus; import com.eatsfine.eatsfine.domain.table_layout.repository.TableLayoutRepository; +import com.eatsfine.eatsfine.domain.tableblock.exception.TableBlockException; 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.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 +36,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 TableBlockException(TableLayoutErrorStatus._CANNOT_DELETE_LAYOUT_WITH_FUTURE_BOOKINGS); + } + + // 미래 예약이 없으면 배치도 재생성 + deactivateExistingLayout(store); + } // 새 배치도 생성 TableLayout newLayout = TableLayout.builder() @@ -53,4 +69,15 @@ 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(); + + return layout.getTables().stream() + .anyMatch(table -> + bookingRepository.existsFutureBookingByTable(table.getId(), currentDate, currentTime) + ); + } } From 38eed42b63cf9276a947fde1acbc9b3ed8b6c7fd Mon Sep 17 00:00:00 2001 From: sonjunkyu Date: Thu, 12 Feb 2026 14:06:01 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[REFACTOR]:=20CodeRabbit=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=20-=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EB=B0=B0=EC=B9=98=EB=8F=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=A4=91=EB=B3=B5=20=EC=BF=BC=EB=A6=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=AF=B8?= =?UTF-8?q?=EB=9E=98=20=EC=98=88=EC=95=BD=20=EB=82=B4=EC=97=AD=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=20N+1=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booking/repository/BookingRepository.java | 12 ++++++++ .../TableLayoutCommandServiceImpl.java | 30 ++++++++++--------- 2 files changed, 28 insertions(+), 14 deletions(-) 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/service/TableLayoutCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java index f7d0e7a1..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 @@ -3,19 +3,21 @@ import com.eatsfine.eatsfine.domain.booking.repository.BookingRepository; import com.eatsfine.eatsfine.domain.store.entity.Store; 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 com.eatsfine.eatsfine.domain.tableblock.exception.TableBlockException; 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 @@ -43,11 +45,11 @@ public TableLayoutResDto.LayoutDetailDto createLayout( boolean hasFutureBookings = checkFutureBookingsInLayout(existingLayout.get()); if (hasFutureBookings) { - throw new TableBlockException(TableLayoutErrorStatus._CANNOT_DELETE_LAYOUT_WITH_FUTURE_BOOKINGS); + throw new TableLayoutException(TableLayoutErrorStatus._CANNOT_DELETE_LAYOUT_WITH_FUTURE_BOOKINGS); } - // 미래 예약이 없으면 배치도 재생성 - deactivateExistingLayout(store); + // 미래 예약이 없으면 배치도 비활성화 후 재생성 + tableLayoutRepository.delete(existingLayout.get()); } // 새 배치도 생성 @@ -64,20 +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 layout.getTables().stream() - .anyMatch(table -> - bookingRepository.existsFutureBookingByTable(table.getId(), currentDate, currentTime) - ); + return !tableIdsWithFutureBookings.isEmpty(); } }