Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,28 @@ List<Booking> findActiveBookingsByTableAndDate(
* @return 만료된 예약 리스트
*/
List<Booking> findAllByStatusAndCreatedAtBefore(BookingStatus status, LocalDateTime threshold);


/**
* 특정 식당의 브레이크 타임과 겹치는 가장 늦은 예약 날짜를 조회합니다.
* @param adjustedBreakStart 브레이크 시작 시간에서 식당의 예약 간격(bookingIntervalMinutes)을 뺀 시간
*/
Comment on lines +83 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Javadoc에 adjustedBreakStart 설명이 추가되었으나, 다른 파라미터 문서가 누락되어 있습니다.

adjustedBreakStart에 대한 설명이 추가된 것은 좋습니다. 다만 breakStart, breakEnd, storeId 파라미터에 대한 @param 문서도 함께 기술하면 메서드 시그니처의 전체적인 이해가 쉬워집니다. 특히 breakStartbreakEnd가 자정 교차 가능 여부에 대한 제약 조건도 명시하면 좋겠습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/booking/repository/BookingRepository.java`
around lines 83 - 86, Update the Javadoc for the method in BookingRepository
(referencing parameters adjustedBreakStart, breakStart, breakEnd, storeId) to
include missing `@param` entries: document breakStart and breakEnd (including
whether they may cross midnight and how that is handled), describe storeId as
the target restaurant identifier, and keep the existing adjustedBreakStart
description (already explaining it's breakStart minus bookingIntervalMinutes);
ensure the `@param` wording clearly states units (e.g., LocalTime or minutes) and
any assumptions used by the method.

@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) " +
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

상태 필터에서 FQCN 열거형 사용이 같은 파일 내 다른 쿼리들과 일관성이 없습니다.

같은 파일 내의 다른 쿼리들(Line 28, 35, 43, 58, 67)은 모두 'CONFIRMED', 'PENDING' 문자열 리터럴을 사용하고 있지만, 이 쿼리만 com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.CONFIRMED FQCN을 사용합니다.

FQCN 방식이 타입 안전성 면에서 더 나은 접근이지만, 파일 내 일관성을 위해 한쪽으로 통일하는 것을 권장합니다. 만약 FQCN 방식을 채택한다면 다른 쿼리들도 함께 마이그레이션하는 것이 좋겠습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/booking/repository/BookingRepository.java`
at line 89, This query in BookingRepository uses the FQCN
com.eatsfine.eatsfine.domain.booking.enums.BookingStatus.CONFIRMED/PENDING while
other queries in the same file use string literals ('CONFIRMED','PENDING'); make
them consistent by replacing the FQCN usage in this query with the same string
literals ('CONFIRMED','PENDING') or alternatively migrate the other queries to
use the BookingStatus FQCN consistently—update the condition in the repository
query accordingly and ensure references to BookingStatus (enum) are uniform
across BookingRepository.

"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<LocalDate> findLastConflictingDate(
@Param("storeId") Long storeId,
@Param("breakStart") LocalTime breakStart,
@Param("breakEnd") LocalTime breakEnd,
@Param("adjustedBreakStart") LocalTime adjustedBreakStart
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -51,6 +52,14 @@ public ApiResponse<BusinessHoursResDto.UpdateBreakTimeDto> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
){}

Expand All @@ -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
){}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -38,6 +40,9 @@ public record UpdateBreakTimeDto(
LocalTime breakStartTime,

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm")
LocalTime breakEndTime
LocalTime breakEndTime,

@JsonIgnore
LocalDate effectiveDate
Comment on lines +45 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

응답 DTO에 @JsonIgnore 내부 전용 필드 혼재.

effectiveDate는 클라이언트에 노출되지 않는 내부 판단용 필드입니다. 응답 DTO에 @JsonIgnore 필드를 추가하는 것은 DTO의 역할(직렬화 대상 데이터 전달)과 맞지 않습니다. 서비스 계층에서 별도의 내부 결과 객체를 반환하고 컨트롤러에서 응답 DTO로 변환하는 구조가 더 명확합니다.

현재 규모에서는 실용적인 접근이지만, 향후 필드가 늘어나면 분리를 고려해 주세요.

🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/businesshours/dto/BusinessHoursResDto.java`
around lines 45 - 46, BusinessHoursResDto contains an internal-only field
effectiveDate annotated with `@JsonIgnore` which mixes internal logic into a
response DTO; remove effectiveDate from BusinessHoursResDto and move it into a
separate internal result/model (e.g., BusinessHoursInternalResult or include it
in the service return type) produced by the service method that currently
returns BusinessHoursResDto, then map/transform that internal result to
BusinessHoursResDto in the controller (or provide a toDto mapper) so the DTO
only contains fields meant for JSON responses.

){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.*;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;

@Entity
Expand Down Expand Up @@ -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;
}
}
Comment on lines +69 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

LocalDate.now() 직접 호출은 테스트 용이성을 저해합니다.

엔티티 내부에서 LocalDate.now()를 직접 호출하면 단위 테스트 시 현재 날짜를 제어할 수 없습니다. effectiveDate와 비교할 기준 날짜를 파라미터로 받거나, Clock을 주입하는 방식을 고려해 주세요.

♻️ 파라미터로 기준 날짜를 전달하는 방식 제안
-    public void updateBreakTime(LocalTime startTime, LocalTime endTime, LocalDate effectiveDate) {
-        // 오늘(혹은 과거) 날짜라면 -> 즉시 반영
-        if (effectiveDate == null || !effectiveDate.isAfter(LocalDate.now())) {
+    public void updateBreakTime(LocalTime startTime, LocalTime endTime, LocalDate effectiveDate, LocalDate today) {
+        // 오늘(혹은 과거) 날짜라면 -> 즉시 반영
+        if (effectiveDate == null || !effectiveDate.isAfter(today)) {
🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java`
around lines 69 - 85, The method updateBreakTime currently calls LocalDate.now()
directly which hinders testing; change updateBreakTime(LocalTime startTime,
LocalTime endTime, LocalDate effectiveDate) to accept a reference date (e.g.,
add a parameter LocalDate referenceDate) and replace LocalDate.now() with that
referenceDate in the comparison, updating the immediate vs. scheduled branches
that set breakStartTime, breakEndTime, newBreakStartTime, newBreakEndTime, and
effectiveDate accordingly; update all callers/tests to pass a controllable
reference date (or add an overloaded convenience method that delegates to the
new signature for backward compatibility).


// 대기열 -> 실제 반영 (스케줄러 호출)
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BusinessHours, Long> {
Optional<BusinessHours> findByStoreAndDayOfWeek(Store store, DayOfWeek dayOfWeek);
List<BusinessHours> findAllByEffectiveDateLessThanEqualAndEffectiveDateIsNotNull(LocalDate date);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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);
}
Comment on lines 73 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

null 체크가 검증 루프 이후에 위치하여 불필요한 반복이 발생합니다.

breakStartTime/breakEndTime이 null인 경우, BreakTimeValidator는 조기 반환하므로 Lines 73-83의 루프가 모든 요일에 대해 무의미하게 실행됩니다. null 체크(Lines 85-97)를 검증 루프 앞으로 이동하면 가독성과 효율성이 모두 향상됩니다.

♻️ 순서 변경 제안
         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);
+        }
+
+        // 한쪽만 null인 비정상 요청 방어
+        if (dto.breakStartTime() == null || dto.breakEndTime() == null) {
+            throw new BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME);
+        }
+
         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()) {
-                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());

As per coding guidelines, "불필요한 DB 쿼리 호출, N+1 문제 가능성이 있는지 확인."

🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java`
around lines 73 - 97, Move the null-check for
dto.breakStartTime()/dto.breakEndTime() to before the validation loop in
BusinessHoursCommandServiceImpl to avoid pointless iteration over
store.getBusinessHours() when the request is to clear break times or is invalid;
specifically, first handle the "both null" case by updating each BusinessHours
via bh.updateBreakTime(null, null, LocalDate.now()) and return the DTO, then
guard the "one null" case by throwing
BusinessHoursException(BusinessHoursErrorStatus._INVALID_BREAK_TIME); only after
these checks call BreakTimeValidator.validateBreakTime(...) for each non-closed
BusinessHours and catch/ log BusinessHoursException as currently done.


LocalTime adjustedBreakStart = dto.breakStartTime().minusMinutes(store.getBookingIntervalMinutes());

// 1. 예약 충돌 확인
Optional<LocalDate> lastConflictDate = bookingRepository.findLastConflictingDate(
storeId, dto.breakStartTime(), dto.breakEndTime(), adjustedBreakStart
);
Comment on lines +99 to +104
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

adjustedBreakStart가 자정을 넘어 래핑되는 경우에 대한 방어가 없습니다.

BookingRepository.findLastConflictingDate 리뷰에서 언급한 것과 동일한 이슈입니다. breakStartTime이 자정 직후이고 bookingIntervalMinutes가 해당 시간보다 크면, LocalTime.minusMinutes()가 전날 23:xx로 래핑됩니다. 이 경우 JPQL의 Case B 범위 조건(>= adjustedBreakStart AND < breakStart)이 빈 범위가 되어 충돌을 감지하지 못합니다.

래핑 발생 여부를 감지하는 방어 코드를 추가하세요:

🛡️ 방어 코드 예시
  LocalTime adjustedBreakStart = dto.breakStartTime().minusMinutes(store.getBookingIntervalMinutes());
+ // adjustedBreakStart가 breakStart보다 크면 자정 래핑 발생 → Case B 무효화
+ if (adjustedBreakStart.isAfter(dto.breakStartTime())) {
+     adjustedBreakStart = LocalTime.MIDNIGHT; // 또는 Case B를 건너뛰도록 breakStart와 동일하게 설정
+ }
🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java`
around lines 99 - 104, The computed adjustedBreakStart can wrap past midnight
(adjustedBreakStart.isAfter(dto.breakStartTime())), so detect that case and
handle it by splitting the conflict check into two ranges instead of a single
JPQL range: when wrapping, call bookingRepository for the late-night range
(adjustedBreakStart .. 23:59:59) and for the early-morning range (00:00 ..
dto.breakStartTime()) and combine their results (e.g., take the latest
Optional<LocalDate> from both); otherwise keep the existing single call to
bookingRepository.findLastConflictingDate(storeId, dto.breakStartTime(),
dto.breakEndTime(), adjustedBreakStart). Ensure you reference
adjustedBreakStart, dto.breakStartTime(), store.getBookingIntervalMinutes(), and
bookingRepository.findLastConflictingDate when implementing the fix.

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);
}
}
Original file line number Diff line number Diff line change
@@ -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<BusinessHours> 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());
Comment on lines +31 to +52
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

failCountwarnCount가 선언/집계되지만 최종 로그에 포함되지 않습니다.

Line 52의 최종 로그에서 successCount만 출력합니다. 운영 모니터링을 위해 실패 건수와 데이터 불일치 건수도 함께 기록해야 합니다.

🔧 로그 개선 제안
-        log.info("[Scheduler] 반영 작업 완료. (성공: {}/{} 건)", successCount, pendingList.size());
+        log.info("[Scheduler] 반영 작업 완료. (성공: {}건, 실패: {}건, 데이터 불일치: {}건 / 총 {}건)", successCount, failCount, warnCount, pendingList.size());

As per coding guidelines, "예외가 적절히 처리되었는지 확인해줘. (try-catch, throws, ExceptionAdvice)"

🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursScheduler.java`
around lines 31 - 52, The final log in BusinessHoursScheduler currently only
prints successCount; update the end-of-loop log (after the for over pendingList
and transactionTemplate.execute calls to processEachPendingTime) to include
failCount and warnCount as well (e.g., "[Scheduler] 반영 작업 완료. (성공: {}/{} 건, 실패:
{} 건, 불일치: {} 건)") so the aggregated failCount and warnCount variables are
surfaced for operations monitoring; keep the existing successCount increments
and exception handling (catch block logging the exception for BH id) intact.

}

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 리턴
}
}
Loading