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..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 @@ -78,4 +78,28 @@ List findActiveBookingsByTableAndDate( * @return 만료된 예약 리스트 */ 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) " + + "and b.bookingDate >= CURRENT_DATE " + + "and (" + + " (b.bookingTime >= :breakStart and b.bookingTime < :breakEnd) " + // 1. 브레이크 타임 내 시작 + " OR " + + " (: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( + @Param("storeId") Long storeId, + @Param("breakStart") LocalTime breakStart, + @Param("breakEnd") LocalTime breakEnd, + @Param("adjustedBreakStart") LocalTime adjustedBreakStart + ); } 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/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/BusinessHoursReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursReqDto.java index 10db26c9..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 @@ -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/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 ){} } 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..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 @@ -6,6 +6,7 @@ import lombok.*; import java.time.DayOfWeek; +import java.time.LocalDate; import java.time.LocalTime; @Entity @@ -39,25 +40,66 @@ 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; + } + } + + 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 48e96b9b..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 @@ -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 findAllByEffectiveDateLessThanEqualAndEffectiveDateIsNotNull(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 f95cfa42..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 @@ -1,9 +1,12 @@ 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; 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; @@ -12,16 +15,23 @@ 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; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + @Service @Transactional @RequiredArgsConstructor +@Slf4j public class BusinessHoursCommandServiceImpl implements BusinessHoursCommandService { private final StoreRepository storeRepository; private final StoreValidator storeValidator; + private final BookingRepository bookingRepository; @Override public BusinessHoursResDto.UpdateBusinessHoursDto updateBusinessHours( @@ -59,17 +69,52 @@ public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( Store store = storeValidator.validateStoreOwner(storeId, email); + 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; + } + } + + // 브레이크 타임 해제 요청인 경우 (두 시간 모두 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); + } + // 한쪽만 null인 비정상 요청 방어 + if (dto.breakStartTime() == null || dto.breakEndTime() == null) { + throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); + } + + LocalTime adjustedBreakStart = dto.breakStartTime().minusMinutes(store.getBookingIntervalMinutes()); + + // 1. 예약 충돌 확인 + Optional lastConflictDate = bookingRepository.findLastConflictingDate( + storeId, dto.breakStartTime(), dto.breakEndTime(), adjustedBreakStart + ); + LocalDate effectiveDate; + + effectiveDate = lastConflictDate.map( + localDate -> localDate.plusDays(1)) // 예약이 있으면 그 다음날 부터 + .orElseGet(LocalDate::now); // 예약 없으면 오늘부터 + + 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/service/BusinessHoursScheduler.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java new file mode 100644 index 00000000..08186465 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java @@ -0,0 +1,68 @@ +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; + +@Component +@RequiredArgsConstructor +@Slf4j +public class BusinessHoursScheduler { + + private final BusinessHoursRepository businessHoursRepository; + private final TransactionTemplate transactionTemplate; + + @Scheduled(cron = "0 0 0 * * *") + public void applyPendingBreakTimes() { + log.info("[Scheduler] 브레이크 타임 지연 반영 작업 시작"); + + List pendingList = businessHoursRepository.findAllByEffectiveDateLessThanEqualAndEffectiveDateIsNotNull(LocalDate.now()); + + int successCount = 0; + int failCount = 0; + int warnCount = 0; // 데이터 불일치(XOR) 건수 + + // 전체 대상 건수 로그 + log.info("[Scheduler] 처리 대상 건수: {}건", pendingList.size()); + + for (BusinessHours bh : pendingList) { + try { + 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: {}", bh.getId(), e); + } + } + log.info("[Scheduler] 반영 작업 완료. (성공: {}/{} 건)", successCount, pendingList.size()); + } + + 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 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 3b31454f..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 @@ -16,8 +16,9 @@ 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", "브레이크타임 시작 시간은 종료 시간보다 빨라야 합니다."), - _BREAK_TIME_OUT_OF_BUSINESS_HOURS(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4007", "브레이크타임은 영업시간 내에만 설정할 수 있습니다."), + _INVALID_BREAK_TIME(HttpStatus.BAD_REQUEST, "BUSINESS_HOURS4006", "브레이크타임 시작 시간은 종료 시간보다 빨라야 합니다.(심야 영업 시간 고려 필수)"), + _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/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; 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..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 @@ -10,20 +10,36 @@ 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)) { - throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); + // 24시간 영업이면 모든 브레이크 타임 X + if (openTime.equals(closeTime)) { + throw new BusinessHoursException(BusinessHoursErrorStatus._BREAK_TIME_NOT_ALLOWED_FOR_24H); } - // 브레이크타임이 영업시간 내에 존재 - if(breakStartTime.isBefore(openTime) || breakEndTime.isAfter(closeTime)) { - throw new BusinessHoursException(BusinessHoursErrorStatus._BREAK_TIME_OUT_OF_BUSINESS_HOURS); + // 1. 브레이크 시작 < 종료 검증 (자정을 넘기는 브레이크 타임은 없다고 가정) + if (!breakEndTime.isAfter(breakStartTime)) { + throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); } + // 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 6313b7e9..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 @@ -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 ec3623cb..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 @@ -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,55 @@ 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 -> isEffectiveOpen(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 -> isEffectiveOpen(bh, time, false)) + .orElse(false); + } - }).orElse(false); // 현재 요일에 해당하는 영업시간 없으면 닫힘처리 + private boolean isEffectiveOpen(BusinessHours bh, LocalTime time, boolean isToday) { + if (bh.isClosed()) return false; + + LocalTime open = bh.getOpenTime(); + LocalTime close = bh.getCloseTime(); + + boolean isWithinBusinessHours; + + // 1. 영업 시간 범위 먼저 체크 + if (open.equals(close)) { + isWithinBusinessHours = isToday; + // 24시간 영업 + } else if (open.isBefore(close)) { + // 일반 영업 (예: 09:00 ~ 18:00) + isWithinBusinessHours = isToday && (!time.isBefore(open) && time.isBefore(close)); + } else { + // 심야 영업 (예: 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; // 영업 시간 자체가 아님 } }