-
Notifications
You must be signed in to change notification settings - Fork 0
[REFACTOR]: TableLayout 엔티티 컬럼 및 테이블 배치도 재생성 시 미래 예약 검증 로직 추가 #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"})} | ||
| ) | ||
|
Comment on lines
+22
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check for migration files and DDL configuration
echo "=== Checking for migration files ==="
find . -type f \( -name "*.sql" -o -name "*.xml" -o -name "*.yaml" -o -name "*.yml" \) 2>/dev/null | grep -iE '(migration|flyway|liquibase|changelog|db)' | head -30
echo ""
echo "=== Checking application configuration files ==="
find . -type f \( -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" \) 2>/dev/null | head -10
echo ""
echo "=== Checking for DDL auto settings ==="
find . -type f \( -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" \) 2>/dev/null -exec grep -H -i 'ddl-auto\|hibernate.hbm2ddl\|flyway\|liquibase' {} \;
echo ""
echo "=== Checking pom.xml or build.gradle for migration dependencies ==="
fd -e pom.xml -e gradle | head -5 | xargs grep -l -i 'flyway\|liquibase' 2>/dev/nullRepository: Eatsfine/BE Length of output: 542 🏁 Script executed: find . -type f -name "TableLayout.java" | head -1 | xargs cat -nRepository: Eatsfine/BE Length of output: 2668 DB 레벨에서 활성 배치도 중복 방지 — 접근 방식은 적절하나 DDL 관리 방식 재검토 필요합니다.
확인/개선 사항:
🤖 Prompt for AI Agents |
||
| 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; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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", "미래 예약이 있는 배치도는 삭제할 수 없습니다. 모든 예약이 완료된 후 재생성해주세요.") | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 메시지가 사용자 동작과 불일치합니다. 사용자는 배치도를 "재생성"하는 동작을 수행하지만, 에러 메시지는 "삭제할 수 없습니다"로 표현되어 있어 혼동을 줄 수 있습니다. 사용자 관점의 동작에 맞게 메시지를 수정하는 것을 권장합니다. 💡 제안- _CANNOT_DELETE_LAYOUT_WITH_FUTURE_BOOKINGS(HttpStatus.BAD_REQUEST, "LAYOUT400_2", "미래 예약이 있는 배치도는 삭제할 수 없습니다. 모든 예약이 완료된 후 재생성해주세요.")
+ _CANNOT_DELETE_LAYOUT_WITH_FUTURE_BOOKINGS(HttpStatus.BAD_REQUEST, "LAYOUT400_2", "미래 예약이 존재하여 배치도를 재생성할 수 없습니다. 모든 예약이 완료된 후 다시 시도해주세요.")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| ; | ||||||
|
|
||||||
| private final HttpStatus httpStatus; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TableLayout> 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()); | ||
| } | ||
|
Comment on lines
+41
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hibernate의 기본 플러시 순서는
새 배치도가 먼저 INSERT되는 시점에 기존 배치도의 해결 방안: 🐛 제안: flush를 명시적으로 호출 // 미래 예약이 없으면 배치도 비활성화 후 재생성
tableLayoutRepository.delete(existingLayout.get());
+ tableLayoutRepository.flush();
}As per coding guidelines, "DB 일관성, 롤백 정책이 올바른지 검토". 🤖 Prompt for AI Agents |
||
|
|
||
| // 새 배치도 생성 | ||
| 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(); | ||
|
Comment on lines
+71
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
날짜와 시간을 별도로 비교하면 자정 경계에서 경합 조건이 발생할 수 있습니다 (예: 🤖 Prompt for AI Agents |
||
| List<Long> tableIds = layout.getTables().stream() | ||
| .map(StoreTable::getId) | ||
| .toList(); | ||
|
|
||
| if (tableIds.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| List<Long> tableIdsWithFutureBookings = bookingRepository.findTableIdsWithFutureBookings(tableIds, currentDate, currentTime); | ||
|
|
||
| return !tableIdsWithFutureBookings.isEmpty(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
이전 N+1 문제를 잘 해결한 벌크 쿼리입니다.
쿼리 로직이 정확하고, 기존
existsFutureBookingByTable단건 호출의 N+1 문제를 해결합니다.한 가지 고려사항: 반환 타입이
List<Long>이지만, 호출부(TableLayoutCommandServiceImpl)에서는isEmpty()여부만 확인합니다. 존재 여부만 필요하다면SELECT COUNT(...) > 0또는 JPQL의EXISTS서브쿼리를 사용하는boolean반환 메서드가 DB 부하를 줄일 수 있습니다.🤖 Prompt for AI Agents