From 469804fe2619582be6fd1569ebb91923731c2e16 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Wed, 11 Feb 2026 14:19:58 +0900 Subject: [PATCH 01/14] =?UTF-8?q?[REFACTOR]:=20=EC=8B=AC=EC=95=BC=20?= =?UTF-8?q?=EC=98=81=EC=97=85=EC=8B=9C=EA=B0=84=EB=8F=84=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validator/BusinessHoursValidator.java | 4 +- .../store/service/StoreQueryServiceImpl.java | 50 +++++++++++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java index 6313b7e9..05797ef3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java @@ -16,14 +16,14 @@ public static void validateForCreate(List dto) { validateDuplicateDayOfWeek(dto); validateOpenDay(dto); validateClosedDay(dto); - validateOpenCloseTime(dto); + //validateOpenCloseTime(dto); } public static void validateForUpdate(List dto) { validateDuplicateDayOfWeek(dto); validateOpenDay(dto); validateClosedDay(dto); - validateOpenCloseTime(dto); + //validateOpenCloseTime(dto); } // 7일 모두 입력 여부 검증 diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java index ec3623cb..7c2530ea 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java @@ -1,5 +1,6 @@ package com.eatsfine.eatsfine.domain.store.service; +import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; import com.eatsfine.eatsfine.domain.store.condition.StoreSearchCondition; import com.eatsfine.eatsfine.domain.store.converter.StoreConverter; import com.eatsfine.eatsfine.domain.store.dto.StoreResDto; @@ -90,20 +91,47 @@ public StoreResDto.GetMainImageDto getMainImage(Long storeId) { // 현재 영업 여부 계산 (실시간 계산) @Override public boolean isOpenNow(Store store, LocalDateTime now) { - DayOfWeek dayOfWeek = now.getDayOfWeek(); + DayOfWeek today = now.getDayOfWeek(); + DayOfWeek yesterday = today.minus(1); + LocalTime time = now.toLocalTime(); - return store.findBusinessHoursByDay(dayOfWeek) - .map(bh -> { - if (bh.isClosed()) return false; + // 1. 오늘 기준 영업 중인지 확인 + boolean openToday = store.findBusinessHoursByDay(today) + .map(bh -> isEffeciveOpen(bh, time, true)) + .orElse(false); + + if (openToday) return true; - if ((bh.getBreakStartTime() != null && bh.getBreakEndTime() != null)) { - if (!time.isBefore(bh.getBreakStartTime()) && (time.isBefore(bh.getBreakEndTime()))) { - return false; // start <= time < end 에 쉼 - } - } - return (!time.isBefore(bh.getOpenTime()) && time.isBefore(bh.getCloseTime())); + // 2. 어제 시작된 심야 영업이 아직 종료되지 않았는지 확인 + return store.findBusinessHoursByDay(yesterday) + .map(bh -> isEffeciveOpen(bh, time, false)) + .orElse(false); + } - }).orElse(false); // 현재 요일에 해당하는 영업시간 없으면 닫힘처리 + private boolean isEffeciveOpen(BusinessHours bh, LocalTime time, boolean isToday) { + LocalTime open = bh.getOpenTime(); + LocalTime close = bh.getCloseTime(); + + if (bh.isClosed()) return false; + + // 브레이크 타임 체크 (당일 내에서 영업만 체크) + if(isToday && bh.getBreakStartTime() != null && bh.getBreakEndTime() != null) { + if (!time.isBefore(bh.getBreakStartTime()) && time.isBefore(bh.getBreakEndTime())) { + return false; + } + } + + if(open.isBefore(close)) { + // 일반 영업 (예: 09:00 ~ 18:00) + return isToday && (!time.isBefore(open) && time.isBefore(close)); + } else { + // 심야 영업 (예: 23:00 ~ 02:00) + if(isToday) { + return !time.isBefore(open); + } else { + return time.isBefore(close); + } + } } } From d466535104e8ab893c2e4f36f3135867cfd4174b Mon Sep 17 00:00:00 2001 From: twodo0 Date: Wed, 11 Feb 2026 15:02:55 +0900 Subject: [PATCH 02/14] =?UTF-8?q?[REFACTOR]:=20=EC=8B=AC=EC=95=BC=20?= =?UTF-8?q?=EC=98=81=EC=97=85=EC=9D=84=20=EA=B3=A0=EB=A0=A4=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=B8=8C=EB=A0=88=EC=9D=B4=ED=81=AC=20=ED=83=80?= =?UTF-8?q?=EC=9E=84=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validator/BreakTimeValidator.java | 28 ++++++++++++++----- .../validator/BusinessHoursValidator.java | 9 +++--- .../store/service/StoreQueryServiceImpl.java | 14 ++++++---- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java index a8766dd3..2af65d78 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java @@ -10,20 +10,34 @@ public class BreakTimeValidator { public static void validateBreakTime(LocalTime openTime, LocalTime closeTime, LocalTime breakStartTime, LocalTime breakEndTime) { // 휴무일은 검증 대상이 아님 - if(openTime == null || closeTime == null) { + if (openTime == null || closeTime == null || breakStartTime == null || breakEndTime == null) { return; } - // start < end - if(!breakEndTime.isAfter(breakStartTime)) { + // 24시간 영업이면 모든 브레이크 타임 X + if (openTime.equals(closeTime)) return; + + // 1. 브레이크 시작 < 종료 검증 (자정을 넘기는 브레이크 타임은 없다고 가정) + if (!breakEndTime.isAfter(breakStartTime)) { throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); } - // 브레이크타임이 영업시간 내에 존재 - if(breakStartTime.isBefore(openTime) || breakEndTime.isAfter(closeTime)) { - throw new BusinessHoursException(BusinessHoursErrorStatus._BREAK_TIME_OUT_OF_BUSINESS_HOURS); + // 2. 브레이크 타임이 영업시간 내에 있는지 검증 + if (openTime.isBefore(closeTime)) { + // 일반 영업: open <= breakStartTime AND breakEndTime <= close 여야 함 + if (breakStartTime.isBefore(openTime) || breakEndTime.isAfter(closeTime)) { + throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); + } + } else { + // 심야 영업: [close ~ open] 사이(영업 안 하는 시간)에 브레이크 타임이 있으면 에러 + if (!breakStartTime.isBefore(closeTime) && breakStartTime.isBefore(openTime)) { + throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); + } + if (!breakEndTime.isBefore(closeTime) && breakEndTime.isBefore(openTime)) { + throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); + } } - } } + diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java index 05797ef3..d930be43 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BusinessHoursValidator.java @@ -16,14 +16,14 @@ public static void validateForCreate(List dto) { validateDuplicateDayOfWeek(dto); validateOpenDay(dto); validateClosedDay(dto); - //validateOpenCloseTime(dto); + validateOpenCloseTime(dto); } public static void validateForUpdate(List dto) { validateDuplicateDayOfWeek(dto); validateOpenDay(dto); validateClosedDay(dto); - //validateOpenCloseTime(dto); + validateOpenCloseTime(dto); } // 7일 모두 입력 여부 검증 @@ -37,8 +37,9 @@ private static void validateComplete(List dto) { private static void validateOpenCloseTime(List dto) { for(BusinessHoursReqDto.Summary s: dto) { if(!s.isClosed()){ - if(s.openTime().isAfter(s.closeTime())) { - throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BUSINESS_TIME); + // 24시간 영업 허용 + if(s.openTime().equals(s.closeTime())) { + continue; } } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java index 7c2530ea..ec3203b2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java @@ -98,30 +98,34 @@ public boolean isOpenNow(Store store, LocalDateTime now) { // 1. 오늘 기준 영업 중인지 확인 boolean openToday = store.findBusinessHoursByDay(today) - .map(bh -> isEffeciveOpen(bh, time, true)) + .map(bh -> isEffectiveOpen(bh, time, true)) .orElse(false); if (openToday) return true; // 2. 어제 시작된 심야 영업이 아직 종료되지 않았는지 확인 return store.findBusinessHoursByDay(yesterday) - .map(bh -> isEffeciveOpen(bh, time, false)) + .map(bh -> isEffectiveOpen(bh, time, false)) .orElse(false); } - private boolean isEffeciveOpen(BusinessHours bh, LocalTime time, boolean isToday) { + private boolean isEffectiveOpen(BusinessHours bh, LocalTime time, boolean isToday) { LocalTime open = bh.getOpenTime(); LocalTime close = bh.getCloseTime(); if (bh.isClosed()) return false; - // 브레이크 타임 체크 (당일 내에서 영업만 체크) - if(isToday && bh.getBreakStartTime() != null && bh.getBreakEndTime() != null) { + // 브레이크 타임 체크 (어제 오픈한 가게의 새벽 브레이크 타임도 걸러내야 함) + if(bh.getBreakStartTime() != null && bh.getBreakEndTime() != null) { if (!time.isBefore(bh.getBreakStartTime()) && time.isBefore(bh.getBreakEndTime())) { return false; } } + // 24시간 영업 체크 (open == close) + if(open.equals(close)) return true; + + // 영업 시간 체크 if(open.isBefore(close)) { // 일반 영업 (예: 09:00 ~ 18:00) return isToday && (!time.isBefore(open) && time.isBefore(close)); From a652b8fb9609c3cc2425e9252c50e8266e64f852 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Wed, 11 Feb 2026 15:32:19 +0900 Subject: [PATCH 03/14] =?UTF-8?q?[REFACTOR]:=20=EB=B8=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=81=AC=20=ED=83=80=EC=9E=84=20=EA=B2=80=EC=A6=9D=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5?= =?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 --- .../businesshours/dto/BusinessHoursReqDto.java | 7 +++++++ .../service/BusinessHoursCommandServiceImpl.java | 12 +++++++++++- .../status/BusinessHoursErrorStatus.java | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java index 10db26c9..7b9d19e9 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java @@ -1,6 +1,7 @@ package com.eatsfine.eatsfine.domain.businesshours.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Builder; @@ -15,14 +16,18 @@ public class BusinessHoursReqDto { public record Summary( @NotNull(message = "요일은 필수입니다.") + @Schema(description = "요일", example = "MONDAY") DayOfWeek day, + @Schema(description = "영업 시작 시간", type = "string", example = "09:00") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime openTime, + @Schema(description = "영업 종료 시간", type = "string", example = "22:00") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime closeTime, + @Schema(description = "휴무 여부", example = "false") boolean isClosed ){} @@ -36,10 +41,12 @@ public record UpdateBusinessHoursDto( public record UpdateBreakTimeDto( @NotNull(message = "브레이크타임 시작 시간은 필수입니다.") + @Schema(description = "브레이크 시작 시간", type = "string", example = "15:00") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime breakStartTime, @NotNull(message = "브레이크타임 종료 시간은 필수입니다.") + @Schema(description = "브레이크 시작 시간", type = "string", example = "17:00") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime breakEndTime ){} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java index f95cfa42..59d962b2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java @@ -4,6 +4,7 @@ import com.eatsfine.eatsfine.domain.businesshours.dto.BusinessHoursReqDto; import com.eatsfine.eatsfine.domain.businesshours.dto.BusinessHoursResDto; import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; +import com.eatsfine.eatsfine.domain.businesshours.exception.BusinessHoursException; import com.eatsfine.eatsfine.domain.businesshours.validator.BreakTimeValidator; import com.eatsfine.eatsfine.domain.businesshours.validator.BusinessHoursValidator; import com.eatsfine.eatsfine.domain.store.entity.Store; @@ -12,12 +13,14 @@ import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional @RequiredArgsConstructor +@Slf4j public class BusinessHoursCommandServiceImpl implements BusinessHoursCommandService { private final StoreRepository storeRepository; @@ -61,7 +64,14 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( for(BusinessHours bh : store.getBusinessHours()) { if(bh.isClosed()) continue; - BreakTimeValidator.validateBreakTime(bh.getOpenTime(), bh.getCloseTime(), dto.breakStartTime(), dto.breakEndTime()); + try { + BreakTimeValidator.validateBreakTime(bh.getOpenTime(), bh.getCloseTime(), dto.breakStartTime(), dto.breakEndTime()); + } catch (BusinessHoursException e) { + log.error("브레이크 타임 검증 실패 - 요일: {}, 영업시간: {}~{}, 브레이크: {}~{}", + bh.getDayOfWeek(), bh.getOpenTime(), bh.getCloseTime(), + dto.breakStartTime(), dto.breakEndTime()); + throw e; + } } store.getBusinessHours().forEach(s -> { diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java index 3b31454f..13a44b69 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java @@ -16,7 +16,7 @@ public enum BusinessHoursErrorStatus implements BaseErrorCode { _INVALID_OPEN_DAY(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4004", "영업일에는 영업시간 및 마감 시간이 존재해야 합니다."), _INVALID_CLOSED_DAY(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4005", "휴무일에는 영업시간이 존재할 수 없습니다."), _BUSINESS_HOURS_DAY_NOT_FOUND(HttpStatus.NOT_FOUND, "BUSINESS_HOURS404", "해당 요일에 대한 영업시간 정보가 존재하지 않습니다."), - _INVALID_BREAK_TIME(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4006", "브레이크타임 시작 시간은 종료 시간보다 빨라야 합니다."), + _INVALID_BREAK_TIME(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4006", "브레이크타임 시작 시간은 종료 시간보다 빨라야 합니다.(심야 영업 시간 고려 필수)"), _BREAK_TIME_OUT_OF_BUSINESS_HOURS(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4007", "브레이크타임은 영업시간 내에만 설정할 수 있습니다."), ; From dedb64f0098ae36d9d86d48ffa4a6d384977c654 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 01:43:18 +0900 Subject: [PATCH 04/14] =?UTF-8?q?[FEAT]:=20=ED=8A=B9=EC=A0=95=20=EC=8B=9D?= =?UTF-8?q?=EB=8B=B9=EC=9D=98=20=ED=8A=B9=EC=A0=95=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EB=8C=80=EC=99=80=20=EA=B2=B9=EC=B9=98=EB=8A=94=20=EA=B0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=8A=A6=EC=9D=80=20=EC=98=88=EC=95=BD=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EC=B0=BE=EB=8A=94=20=EC=BF=BC=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booking/repository/BookingRepository.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 67a0b5e1..91e9fc79 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 @@ -78,4 +78,18 @@ List findActiveBookingsByTableAndDate( * @return 만료된 예약 리스트 */ List findAllByStatusAndCreatedAtBefore(BookingStatus status, LocalDateTime threshold); + + + // 특정 식당의 특정 시간대와 겹치는 가장 늦은 예약 날짜 찾기 + @Query("select max(b.bookingDate) from Booking b " + + "where b.store.id = :storeId " + + "and b.status IN (com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.CONFIRMED, com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.PENDING) " + + "and b.bookingDate >= CURRENT_DATE " + + "and b.bookingTime >= :breakStart and b.bookingTime < :breakEnd" + ) + Optional findLastConflictingDate( + @Param("storeId") Long storeId, + @Param("breakStart") LocalTime breakStart, + @Param("breakEnd") LocalTime breakEnd + ); } From 205a8d918f04da281afb6814c4a41232f7ba5b46 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 01:44:07 +0900 Subject: [PATCH 05/14] =?UTF-8?q?[FEAT]:=20DTO=EC=97=90=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EC=9D=BC=EC=9E=90=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=BB=A8=EB=B2=84=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../businesshours/converter/BusinessHoursConverter.java | 4 +++- .../domain/businesshours/dto/BusinessHoursResDto.java | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/converter/BusinessHoursConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/converter/BusinessHoursConverter.java index e74b3374..e930ce55 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/converter/BusinessHoursConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/converter/BusinessHoursConverter.java @@ -4,6 +4,7 @@ import com.eatsfine.eatsfine.domain.businesshours.dto.BusinessHoursResDto; import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; +import java.time.LocalDate; import java.util.List; public class BusinessHoursConverter { @@ -49,11 +50,12 @@ public static BusinessHoursResDto.UpdateBusinessHoursDto toUpdateBusinessHoursDt .build(); } - public static BusinessHoursResDto.UpdateBreakTimeDto toUpdateBreakTimeDto(Long storeId, BusinessHoursReqDto.UpdateBreakTimeDto dto) { + public static BusinessHoursResDto.UpdateBreakTimeDto toUpdateBreakTimeDto(Long storeId, BusinessHoursReqDto.UpdateBreakTimeDto dto, LocalDate effectiveDate) { return BusinessHoursResDto.UpdateBreakTimeDto.builder() .storeId(storeId) .breakStartTime(dto.breakStartTime()) .breakEndTime(dto.breakEndTime()) + .effectiveDate(effectiveDate) .build(); } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursResDto.java index d7674866..954b0091 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursResDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursResDto.java @@ -1,9 +1,11 @@ package com.eatsfine.eatsfine.domain.businesshours.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Builder; import java.time.DayOfWeek; +import java.time.LocalDate; import java.time.LocalTime; import java.util.List; @@ -38,6 +40,9 @@ public record UpdateBreakTimeDto( LocalTime breakStartTime, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") - LocalTime breakEndTime + LocalTime breakEndTime, + + @JsonIgnore + LocalDate effectiveDate ){} } From d1bf0ae93debdcf62117d6dec1891c6ea7bfd44d Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 01:44:57 +0900 Subject: [PATCH 06/14] =?UTF-8?q?[FEAT]:=20=EB=B8=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=81=AC=ED=83=80=EC=9E=84=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../businesshours/entity/BusinessHours.java | 46 +++++++++++++++++-- .../service/BusinessHoursScheduler.java | 36 +++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java index b676360c..d19b720b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java @@ -6,6 +6,7 @@ import lombok.*; import java.time.DayOfWeek; +import java.time.LocalDate; import java.time.LocalTime; @Entity @@ -39,25 +40,60 @@ public class BusinessHours extends BaseEntity { @Column(name = "break_end_time") private LocalTime breakEndTime; + @Column(name = "new_break_start_time") + private LocalTime newBreakStartTime; + + @Column(name = "new_break_end_time") + private LocalTime newBreakEndTime; + + @Column(name = "effective_date") + private LocalDate effectiveDate; + // 휴일 여부 (특정 요일 고정 휴무) @Builder.Default @Column(name = "is_closed", nullable = false) private boolean isClosed = false; - public void assignStore(Store store){ + public void assignStore(Store store) { this.store = store; } // 영업시간 변경 - public void update(LocalTime open, LocalTime close, boolean isClosed){ + public void update(LocalTime open, LocalTime close, boolean isClosed) { this.openTime = open; this.closeTime = close; this.isClosed = isClosed; } // 브레이크타임 변경 - public void updateBreakTime(LocalTime breakStart, LocalTime breakEnd){ - this.breakStartTime = breakStart; - this.breakEndTime = breakEnd; + public void updateBreakTime(LocalTime startTime, LocalTime endTime, LocalDate effectiveDate) { + // 오늘(혹은 과거) 날짜라면 -> 즉시 반영 + if (effectiveDate == null || !effectiveDate.isAfter(LocalDate.now())) { + this.breakStartTime = startTime; + this.breakEndTime = endTime; + // 대기 중인 데이터 초기화 + this.newBreakStartTime = null; + this.newBreakEndTime = null; + this.effectiveDate = null; + } + // 미래 날짜라면 -> 대기열에 저장 + else { + this.newBreakStartTime = startTime; + this.newBreakEndTime = endTime; + this.effectiveDate = effectiveDate; + } + } + + // 대기열 -> 실제 반영 (스케줄러 호출) + public void applyPendingBreakTime() { + if (this.newBreakStartTime != null && this.newBreakEndTime != null) { + this.breakStartTime = newBreakStartTime; + this.breakEndTime = newBreakEndTime; + + // 반영 후 대기열 비우기 + this.newBreakStartTime = null; + this.newBreakEndTime = null; + this.effectiveDate = null; + } } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java new file mode 100644 index 00000000..13901ea2 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java @@ -0,0 +1,36 @@ +package com.eatsfine.eatsfine.domain.businesshours.service; + +import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; +import com.eatsfine.eatsfine.domain.businesshours.repository.BusinessHoursRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class BusinessHoursScheduler { + + private final BusinessHoursRepository businessHoursRepository; + + @Scheduled(cron = "0 0 0 * * *") // 매일 자정 + @Transactional + public void applyPendingBreakTimes() { + log.info("[Scheduler] 브레이크 타임 지연 반영 작업 시작"); + + List targets = businessHoursRepository.findAllByEffectiveDate(LocalDate.now()); + + if(targets.isEmpty()) { + log.info("[Scheduler] 오늘 반영할 항목이 없습니다."); + return; + } + + targets.forEach(BusinessHours::applyPendingBreakTime); + log.info("[Scheduler] 총 {}건의 브레이크 타임이 성공적으로 갱신되었습니다.", targets.size()); + } +} From 60f43a52b7301fbc8dcbd7d331596f5747009656 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 01:45:50 +0900 Subject: [PATCH 07/14] =?UTF-8?q?[FEAT]:=20=EB=B8=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=81=AC=20=ED=83=80=EC=9E=84=20=EC=84=A4=EC=A0=95=20=EC=8B=9C?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=20=EC=8B=9C=EA=B0=84=EB=8C=80=EC=99=80?= =?UTF-8?q?=EC=9D=98=20=EC=B6=A9=EB=8F=8C=20=EB=B0=A9=EC=96=B4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BusinessHoursController.java | 9 ++++++ .../repository/BusinessHoursRepository.java | 3 ++ .../BusinessHoursCommandServiceImpl.java | 29 +++++++++++++++++-- .../status/BusinessHoursSuccessStatus.java | 3 +- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java index 4e16fbe0..0d298e7a 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*; import java.security.Principal; +import java.time.LocalDate; @Tag(name = "BusinessHours", description = "영업시간 관련 API") @RequestMapping("/api/v1") @@ -51,6 +52,14 @@ public ApiResponse updateBreakTime( @RequestBody BusinessHoursReqDto.UpdateBreakTimeDto dto, @CurrentUser User user ){ + BusinessHoursResDto.UpdateBreakTimeDto result = businessHoursCommandService.updateBreakTime(storeId, dto, user.getUsername()); + + // 1. 날짜가 미래라면 DELAYED(지연) 코드 반환 + if (result.effectiveDate().isAfter(LocalDate.now())) { + return ApiResponse.of( + BusinessHoursSuccessStatus._UPDATE_BREAKTIME_DELAYED, + result); + } return ApiResponse.of( BusinessHoursSuccessStatus._UPDATE_BREAKTIME_SUCCESS, businessHoursCommandService.updateBreakTime(storeId, dto, user.getUsername()) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java index 48e96b9b..7b3a9a43 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java @@ -5,8 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; public interface BusinessHoursRepository extends JpaRepository { Optional findByStoreAndDayOfWeek(Store store, DayOfWeek dayOfWeek); + List findAllByEffectiveDate(LocalDate date); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java index 59d962b2..514fb44a 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java @@ -1,5 +1,6 @@ package com.eatsfine.eatsfine.domain.businesshours.service; +import com.eatsfine.eatsfine.domain.booking.repository.BookingRepository; import com.eatsfine.eatsfine.domain.businesshours.converter.BusinessHoursConverter; import com.eatsfine.eatsfine.domain.businesshours.dto.BusinessHoursReqDto; import com.eatsfine.eatsfine.domain.businesshours.dto.BusinessHoursResDto; @@ -17,6 +18,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.util.Optional; + @Service @Transactional @RequiredArgsConstructor @@ -25,6 +29,7 @@ public class BusinessHoursCommandServiceImpl implements BusinessHoursCommandServ private final StoreRepository storeRepository; private final StoreValidator storeValidator; + private final BookingRepository bookingRepository; @Override public BusinessHoursResDto.UpdateBusinessHoursDto updateBusinessHours( @@ -62,6 +67,26 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( Store store = storeValidator.validateStoreOwner(storeId, email); + // 브레이크 타임 해제 요청인 경우 (두 시간 모두 null) + if (dto.breakStartTime() == null && dto.breakEndTime() == null) { + for(BusinessHours bh : store.getBusinessHours()) { + bh.updateBreakTime(null, null, LocalDate.now()); + } + return BusinessHoursConverter.toUpdateBreakTimeDto(storeId, dto, null); + + } + + // 1. 예약 충돌 확인 + Optional lastConflictDate = bookingRepository.findLastConflictingDate( + storeId, dto.breakStartTime(), dto.breakEndTime() + ); + LocalDate effectiveDate; + + effectiveDate = lastConflictDate.map( + localDate -> localDate.plusDays(1)) // 예약이 있으면 그 다음날 부터 + .orElseGet(LocalDate::now); // 예약 없으면 오늘부터 + + for(BusinessHours bh : store.getBusinessHours()) { if(bh.isClosed()) continue; try { @@ -76,10 +101,10 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( store.getBusinessHours().forEach(s -> { if(!s.isClosed()) { - s.updateBreakTime(dto.breakStartTime(), dto.breakEndTime()); + s.updateBreakTime(dto.breakStartTime(), dto.breakEndTime(), effectiveDate); } }); - return BusinessHoursConverter.toUpdateBreakTimeDto(storeId, dto); + return BusinessHoursConverter.toUpdateBreakTimeDto(storeId, dto, effectiveDate); } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursSuccessStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursSuccessStatus.java index b0a12e45..1a1b14bd 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursSuccessStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursSuccessStatus.java @@ -11,7 +11,8 @@ public enum BusinessHoursSuccessStatus implements BaseCode { _UPDATE_BUSINESS_HOURS_SUCCESS(HttpStatus.OK, "BUSINESS_HOURS200", "영업시간이 성공적으로 수정되었습니다."), - _UPDATE_BREAKTIME_SUCCESS(HttpStatus.OK, "BUSINESS_HOURS2001", "브레이크타임이 성공적으로 설정되었습니다.") + _UPDATE_BREAKTIME_SUCCESS(HttpStatus.OK, "BUSINESS_HOURS2001", "브레이크타임이 성공적으로 설정되었습니다."), + _UPDATE_BREAKTIME_DELAYED(HttpStatus.OK, "BUSINESS_HOURS2002", "기존 예약이 존재하여, 마지막 예약 종료 후 다음 날부터 반영됩니다.") ; private final HttpStatus httpStatus; From 6ef6448cb46aac72e059dfc049b26c35f1149321 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 03:04:46 +0900 Subject: [PATCH 08/14] =?UTF-8?q?[FEAT]:=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=EA=B0=80=20=EB=86=93=EC=B9=9C=20PENDING=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=8F=84=20=EC=B2=98=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BusinessHoursScheduler.java | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java index 13901ea2..42d3e4bc 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java @@ -18,19 +18,37 @@ public class BusinessHoursScheduler { private final BusinessHoursRepository businessHoursRepository; - @Scheduled(cron = "0 0 0 * * *") // 매일 자정 - @Transactional + @Scheduled(cron = "0 0 0 * * *") public void applyPendingBreakTimes() { log.info("[Scheduler] 브레이크 타임 지연 반영 작업 시작"); - List targets = businessHoursRepository.findAllByEffectiveDate(LocalDate.now()); + List pendingList = businessHoursRepository.findAllByEffectiveDateLessThanEqualAndEffectiveDateIsNotNull(LocalDate.now()); - if(targets.isEmpty()) { - log.info("[Scheduler] 오늘 반영할 항목이 없습니다."); - return; + // 전체 대상 건수 로그 + log.info("[Scheduler] 처리 대상 건수: {}건", pendingList.size()); + + int successCount = 0; + + for (BusinessHours bh : pendingList) { + try { + processEachPendingTime(bh); + successCount++; + + } catch (Exception e) { + // 개별 건 처리 중 에러 발생 시 로그 남기고 다음 건 진행 + log.error("[Scheduler Exception] 반영 실패 - BH ID: {}, Error: {}", bh.getId(), e.getMessage()); + } } + log.info("[Scheduler] 반영 작업 완료. (성공: {}/{} 건)", successCount, pendingList.size()); + } - targets.forEach(BusinessHours::applyPendingBreakTime); - log.info("[Scheduler] 총 {}건의 브레이크 타임이 성공적으로 갱신되었습니다.", targets.size()); + @Transactional + public void processEachPendingTime(BusinessHours bh) { + if((bh.getNewBreakStartTime() == null) ^ (bh.getNewBreakEndTime() == null)) { + log.warn("[XOR Error] ID: {}", bh.getId()); + bh.clearPendingBreakTime(); + return; + } + bh.applyPendingBreakTime(); } } From 16932bcb79431c0974bee0c32b8d0eef7130dfae Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 03:05:35 +0900 Subject: [PATCH 09/14] =?UTF-8?q?[FEAT]:=2024=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=98=81=EC=97=85=EC=9D=BC=20=EB=95=8C=20=EB=B8=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=81=AC=20=ED=83=80=EC=9E=84=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=95=98=EB=A9=B4=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/businesshours/status/BusinessHoursErrorStatus.java | 2 +- .../domain/businesshours/validator/BreakTimeValidator.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java index 13a44b69..b5b0f5b8 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java @@ -17,7 +17,7 @@ public enum BusinessHoursErrorStatus implements BaseErrorCode { _INVALID_CLOSED_DAY(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4005", "휴무일에는 영업시간이 존재할 수 없습니다."), _BUSINESS_HOURS_DAY_NOT_FOUND(HttpStatus.NOT_FOUND, "BUSINESS_HOURS404", "해당 요일에 대한 영업시간 정보가 존재하지 않습니다."), _INVALID_BREAK_TIME(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4006", "브레이크타임 시작 시간은 종료 시간보다 빨라야 합니다.(심야 영업 시간 고려 필수)"), - _BREAK_TIME_OUT_OF_BUSINESS_HOURS(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4007", "브레이크타임은 영업시간 내에만 설정할 수 있습니다."), + _BREAK_TIME_NOT_ALLOWED_FOR_24H(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4005", "24시간 영업 매장은 브레이크 타임을 설정할 수 없습니다."), ; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java index 2af65d78..eed6a9ef 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java @@ -15,7 +15,9 @@ public static void validateBreakTime(LocalTime openTime, LocalTime closeTime, Lo } // 24시간 영업이면 모든 브레이크 타임 X - if (openTime.equals(closeTime)) return; + if (openTime.equals(closeTime)) { + throw new BusinessHoursException(BusinessHoursErrorStatus._BREAK_TIME_NOT_ALLOWED_FOR_24H); + }; // 1. 브레이크 시작 < 종료 검증 (자정을 넘기는 브레이크 타임은 없다고 가정) if (!breakEndTime.isAfter(breakStartTime)) { From 5662d1e9b02db3a553a0f33b0c2ad11240918959 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 03:06:17 +0900 Subject: [PATCH 10/14] =?UTF-8?q?[FEAT]:=20bookingIntervalTime=20=EA=B3=A0?= =?UTF-8?q?=EB=A0=A4=ED=95=B4=EC=84=9C=20=EC=98=88=EC=95=BD=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/booking/repository/BookingRepository.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 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 91e9fc79..ff777824 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 @@ -85,11 +85,16 @@ List findActiveBookingsByTableAndDate( "where b.store.id = :storeId " + "and b.status IN (com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.CONFIRMED, com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.PENDING) " + "and b.bookingDate >= CURRENT_DATE " + - "and b.bookingTime >= :breakStart and b.bookingTime < :breakEnd" + "and (" + + " (b.bookingTime >= :breakStart and b.bookingTime < :breakEnd) " + // 케이스 A: 브레이크 중에 예약 시작 + " OR " + + " (b.bookingTime >= :adjustedBreakStart and b.bookingTime < :breakStart)" + // 케이스 B: 브레이크 전에 시작해서 걸침 + ")" ) Optional findLastConflictingDate( @Param("storeId") Long storeId, @Param("breakStart") LocalTime breakStart, - @Param("breakEnd") LocalTime breakEnd + @Param("breakEnd") LocalTime breakEnd, + @Param("adjustedBreakStart") LocalTime adjustedBreakStart ); } From d290dbd6f38a3b809e95ac52e20fd54615fb124a Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 03:07:34 +0900 Subject: [PATCH 11/14] =?UTF-8?q?[REFACTOR]:=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BusinessHoursCommandServiceImpl.java | 30 +++++++------ .../store/service/StoreQueryServiceImpl.java | 42 ++++++++++--------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java index 514fb44a..092793a3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java @@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.time.LocalTime; import java.util.Optional; @Service @@ -67,6 +68,19 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( Store store = storeValidator.validateStoreOwner(storeId, email); + + for(BusinessHours bh : store.getBusinessHours()) { + if(bh.isClosed()) continue; + try { + BreakTimeValidator.validateBreakTime(bh.getOpenTime(), bh.getCloseTime(), dto.breakStartTime(), dto.breakEndTime()); + } catch (BusinessHoursException e) { + log.error("브레이크 타임 검증 실패 - 요일: {}, 영업시간: {}~{}, 브레이크: {}~{}", + bh.getDayOfWeek(), bh.getOpenTime(), bh.getCloseTime(), + dto.breakStartTime(), dto.breakEndTime()); + throw e; + } + } + // 브레이크 타임 해제 요청인 경우 (두 시간 모두 null) if (dto.breakStartTime() == null && dto.breakEndTime() == null) { for(BusinessHours bh : store.getBusinessHours()) { @@ -76,9 +90,11 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( } + LocalTime adjustedBreakStart = dto.breakStartTime().minusMinutes(store.getBookingIntervalMinutes()); + // 1. 예약 충돌 확인 Optional lastConflictDate = bookingRepository.findLastConflictingDate( - storeId, dto.breakStartTime(), dto.breakEndTime() + storeId, dto.breakStartTime(), dto.breakEndTime(), adjustedBreakStart ); LocalDate effectiveDate; @@ -87,18 +103,6 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( .orElseGet(LocalDate::now); // 예약 없으면 오늘부터 - for(BusinessHours bh : store.getBusinessHours()) { - if(bh.isClosed()) continue; - try { - BreakTimeValidator.validateBreakTime(bh.getOpenTime(), bh.getCloseTime(), dto.breakStartTime(), dto.breakEndTime()); - } catch (BusinessHoursException e) { - log.error("브레이크 타임 검증 실패 - 요일: {}, 영업시간: {}~{}, 브레이크: {}~{}", - bh.getDayOfWeek(), bh.getOpenTime(), bh.getCloseTime(), - dto.breakStartTime(), dto.breakEndTime()); - throw e; - } - } - store.getBusinessHours().forEach(s -> { if(!s.isClosed()) { s.updateBreakTime(dto.breakStartTime(), dto.breakEndTime(), effectiveDate); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java index ec3203b2..5973ec69 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java @@ -110,32 +110,36 @@ public boolean isOpenNow(Store store, LocalDateTime now) { } private boolean isEffectiveOpen(BusinessHours bh, LocalTime time, boolean isToday) { - LocalTime open = bh.getOpenTime(); - LocalTime close = bh.getCloseTime(); - if (bh.isClosed()) return false; - // 브레이크 타임 체크 (어제 오픈한 가게의 새벽 브레이크 타임도 걸러내야 함) - if(bh.getBreakStartTime() != null && bh.getBreakEndTime() != null) { - if (!time.isBefore(bh.getBreakStartTime()) && time.isBefore(bh.getBreakEndTime())) { - return false; - } - } + LocalTime open = bh.getOpenTime(); + LocalTime close = bh.getCloseTime(); - // 24시간 영업 체크 (open == close) - if(open.equals(close)) return true; + boolean isWithinBusinessHours; - // 영업 시간 체크 - if(open.isBefore(close)) { + // 1. 영업 시간 범위 먼저 체크 + if (open.equals(close)) { + // 24시간 영업 + isWithinBusinessHours = true; + } else if (open.isBefore(close)) { // 일반 영업 (예: 09:00 ~ 18:00) - return isToday && (!time.isBefore(open) && time.isBefore(close)); + isWithinBusinessHours = isToday && (!time.isBefore(open) && time.isBefore(close)); } else { - // 심야 영업 (예: 23:00 ~ 02:00) - if(isToday) { - return !time.isBefore(open); - } else { - return time.isBefore(close); + // 심야 영업 (예: 22:00 ~ 03:00) + isWithinBusinessHours = isToday ? !time.isBefore(open) : time.isBefore(close); + } + + // 2. 영업 시간일 경우에만 브레이크 타임 검사 + if (isWithinBusinessHours) { + if (bh.getBreakStartTime() != null && bh.getBreakEndTime() != null) { + // 브레이크 타임 안에 있으면 false (영업 아님) 반환 + if (!time.isBefore(bh.getBreakStartTime()) && time.isBefore(bh.getBreakEndTime())) { + return false; + } } + return true; // 영업 시간이고 브레이크 타임도 아님 } + + return false; // 영업 시간 자체가 아님 } } From adabe6ddee72da801a2afef6823babb65f967a80 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 03:08:28 +0900 Subject: [PATCH 12/14] =?UTF-8?q?[FEAT]:=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/businesshours/dto/BusinessHoursReqDto.java | 2 +- .../eatsfine/domain/businesshours/entity/BusinessHours.java | 6 ++++++ .../businesshours/repository/BusinessHoursRepository.java | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java index 7b9d19e9..7d796e78 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java @@ -46,7 +46,7 @@ public record UpdateBreakTimeDto( LocalTime breakStartTime, @NotNull(message = "브레이크타임 종료 시간은 필수입니다.") - @Schema(description = "브레이크 시작 시간", type = "string", example = "17:00") + @Schema(description = "브레이크 종료 시간", type = "string", example = "17:00") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime breakEndTime ){} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java index d19b720b..6a0444d3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java @@ -96,4 +96,10 @@ public void applyPendingBreakTime() { this.effectiveDate = null; } } + + public void clearPendingBreakTime() { + this.newBreakStartTime = null; + this.newBreakEndTime = null; + this.effectiveDate = null; + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java index 7b3a9a43..4388a264 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/repository/BusinessHoursRepository.java @@ -11,5 +11,5 @@ public interface BusinessHoursRepository extends JpaRepository { Optional findByStoreAndDayOfWeek(Store store, DayOfWeek dayOfWeek); - List findAllByEffectiveDate(LocalDate date); + List findAllByEffectiveDateLessThanEqualAndEffectiveDateIsNotNull(LocalDate date); } From 828a4086746d507745a2c9d4c03efda4f723744e Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 03:47:28 +0900 Subject: [PATCH 13/14] =?UTF-8?q?[FEAT]:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booking/repository/BookingRepository.java | 5 ++- .../BusinessHoursCommandServiceImpl.java | 6 ++++ .../service/BusinessHoursScheduler.java | 32 +++++++++++++------ .../status/BusinessHoursErrorStatus.java | 3 +- .../validator/BreakTimeValidator.java | 2 +- .../store/service/StoreQueryServiceImpl.java | 2 +- 6 files changed, 37 insertions(+), 13 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 ff777824..cd9b8b20 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 @@ -80,7 +80,10 @@ List findActiveBookingsByTableAndDate( List findAllByStatusAndCreatedAtBefore(BookingStatus status, LocalDateTime threshold); - // 특정 식당의 특정 시간대와 겹치는 가장 늦은 예약 날짜 찾기 + /** + * 특정 식당의 브레이크 타임과 겹치는 가장 늦은 예약 날짜를 조회합니다. + * @param adjustedBreakStart 브레이크 시작 시간에서 식당의 예약 간격(bookingIntervalMinutes)을 뺀 시간 + */ @Query("select max(b.bookingDate) from Booking b " + "where b.store.id = :storeId " + "and b.status IN (com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.CONFIRMED, com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.PENDING) " + diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java index 092793a3..b547f049 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java @@ -6,6 +6,7 @@ import com.eatsfine.eatsfine.domain.businesshours.dto.BusinessHoursResDto; import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; import com.eatsfine.eatsfine.domain.businesshours.exception.BusinessHoursException; +import com.eatsfine.eatsfine.domain.businesshours.status.BusinessHoursErrorStatus; import com.eatsfine.eatsfine.domain.businesshours.validator.BreakTimeValidator; import com.eatsfine.eatsfine.domain.businesshours.validator.BusinessHoursValidator; import com.eatsfine.eatsfine.domain.store.entity.Store; @@ -90,6 +91,11 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( } + // 한쪽만 null인 비정상 요청 방어 + if (dto.breakStartTime() == null || dto.breakEndTime() == null) { + throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); + } + LocalTime adjustedBreakStart = dto.breakStartTime().minusMinutes(store.getBookingIntervalMinutes()); // 1. 예약 충돌 확인 diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java index 42d3e4bc..08186465 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java @@ -1,12 +1,15 @@ package com.eatsfine.eatsfine.domain.businesshours.service; import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; +import com.eatsfine.eatsfine.domain.businesshours.exception.BusinessHoursException; import com.eatsfine.eatsfine.domain.businesshours.repository.BusinessHoursRepository; +import com.eatsfine.eatsfine.domain.businesshours.status.BusinessHoursErrorStatus; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import java.time.LocalDate; import java.util.List; @@ -17,6 +20,7 @@ public class BusinessHoursScheduler { private final BusinessHoursRepository businessHoursRepository; + private final TransactionTemplate transactionTemplate; @Scheduled(cron = "0 0 0 * * *") public void applyPendingBreakTimes() { @@ -24,31 +28,41 @@ public void applyPendingBreakTimes() { List pendingList = businessHoursRepository.findAllByEffectiveDateLessThanEqualAndEffectiveDateIsNotNull(LocalDate.now()); + int successCount = 0; + int failCount = 0; + int warnCount = 0; // 데이터 불일치(XOR) 건수 + // 전체 대상 건수 로그 log.info("[Scheduler] 처리 대상 건수: {}건", pendingList.size()); - int successCount = 0; - for (BusinessHours bh : pendingList) { try { - processEachPendingTime(bh); - successCount++; - + Boolean isApplied = transactionTemplate.execute(status -> processEachPendingTime(bh.getId())); + if(Boolean.TRUE.equals(isApplied)) { + successCount++; + } else { + warnCount++; // XOR 등으로 인해 초기화만 된 경우 + } } catch (Exception e) { + failCount++; // 개별 건 처리 중 에러 발생 시 로그 남기고 다음 건 진행 - log.error("[Scheduler Exception] 반영 실패 - BH ID: {}, Error: {}", bh.getId(), e.getMessage()); + log.error("[Scheduler Exception] 반영 실패 - BH ID: {}", bh.getId(), e); } } log.info("[Scheduler] 반영 작업 완료. (성공: {}/{} 건)", successCount, pendingList.size()); } - @Transactional - public void processEachPendingTime(BusinessHours bh) { + public boolean processEachPendingTime(Long bhId) { + + BusinessHours bh = businessHoursRepository.findById(bhId) + .orElseThrow(() -> new BusinessHoursException(BusinessHoursErrorStatus._BUSINESS_HOURS_NOT_FOUND)); + if((bh.getNewBreakStartTime() == null) ^ (bh.getNewBreakEndTime() == null)) { log.warn("[XOR Error] ID: {}", bh.getId()); bh.clearPendingBreakTime(); - return; + return false; // 초기화만 한 경우 false 리턴 } bh.applyPendingBreakTime(); + return true; // 정상 반영한 경우 true 리턴 } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java index b5b0f5b8..e9d67787 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/status/BusinessHoursErrorStatus.java @@ -17,7 +17,8 @@ public enum BusinessHoursErrorStatus implements BaseErrorCode { _INVALID_CLOSED_DAY(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4005", "휴무일에는 영업시간이 존재할 수 없습니다."), _BUSINESS_HOURS_DAY_NOT_FOUND(HttpStatus.NOT_FOUND, "BUSINESS_HOURS404", "해당 요일에 대한 영업시간 정보가 존재하지 않습니다."), _INVALID_BREAK_TIME(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4006", "브레이크타임 시작 시간은 종료 시간보다 빨라야 합니다.(심야 영업 시간 고려 필수)"), - _BREAK_TIME_NOT_ALLOWED_FOR_24H(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4005", "24시간 영업 매장은 브레이크 타임을 설정할 수 없습니다."), + _BREAK_TIME_NOT_ALLOWED_FOR_24H(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4007", "24시간 영업 매장은 브레이크 타임을 설정할 수 없습니다."), + _BUSINESS_HOURS_NOT_FOUND(HttpStatus.NOT_FOUND, "BUSINESS_HOURS4008", "영업시간 정보가 존재하지 않습니다."), ; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java index eed6a9ef..14b19068 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/validator/BreakTimeValidator.java @@ -17,7 +17,7 @@ public static void validateBreakTime(LocalTime openTime, LocalTime closeTime, Lo // 24시간 영업이면 모든 브레이크 타임 X if (openTime.equals(closeTime)) { throw new BusinessHoursException(BusinessHoursErrorStatus._BREAK_TIME_NOT_ALLOWED_FOR_24H); - }; + } // 1. 브레이크 시작 < 종료 검증 (자정을 넘기는 브레이크 타임은 없다고 가정) if (!breakEndTime.isAfter(breakStartTime)) { diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java index 5973ec69..438946d5 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java @@ -119,8 +119,8 @@ private boolean isEffectiveOpen(BusinessHours bh, LocalTime time, boolean isToda // 1. 영업 시간 범위 먼저 체크 if (open.equals(close)) { + isWithinBusinessHours = isToday; // 24시간 영업 - isWithinBusinessHours = true; } else if (open.isBefore(close)) { // 일반 영업 (예: 09:00 ~ 18:00) isWithinBusinessHours = isToday && (!time.isBefore(open) && time.isBefore(close)); From aa58fc7d6a31ce044e4b540fd1fd669a7e43ec2e Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 12 Feb 2026 04:19:49 +0900 Subject: [PATCH 14/14] =?UTF-8?q?[FEAT]:=20=EC=9E=90=EC=A0=95=20=EB=84=98?= =?UTF-8?q?=EC=96=B4=EA=B0=80=EB=8A=94=20=EB=B8=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=81=AC=20=ED=83=80=EC=9E=84=EC=97=90=20=EC=B6=A9=EB=8F=8C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=98=88=EC=95=BD=20=EC=B0=BE=EC=95=84?= =?UTF-8?q?=EB=82=BC=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/booking/repository/BookingRepository.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 cd9b8b20..a8f8e45c 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 @@ -89,9 +89,11 @@ List findActiveBookingsByTableAndDate( "and b.status IN (com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.CONFIRMED, com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.PENDING) " + "and b.bookingDate >= CURRENT_DATE " + "and (" + - " (b.bookingTime >= :breakStart and b.bookingTime < :breakEnd) " + // 케이스 A: 브레이크 중에 예약 시작 + " (b.bookingTime >= :breakStart and b.bookingTime < :breakEnd) " + // 1. 브레이크 타임 내 시작 " OR " + - " (b.bookingTime >= :adjustedBreakStart and b.bookingTime < :breakStart)" + // 케이스 B: 브레이크 전에 시작해서 걸침 + " (:adjustedBreakStart < :breakStart and b.bookingTime >= :adjustedBreakStart and b.bookingTime < :breakStart) " + // 2. 일반적인 경우 (낮) + " OR " + + " (:adjustedBreakStart > :breakStart and (b.bookingTime >= :adjustedBreakStart or b.bookingTime < :breakStart)) " + // 3. 자정 넘어가는 경우 (밤) ")" ) Optional findLastConflictingDate(