diff --git a/src/main/java/com/foru/freebe/errors/errorcode/ScheduleErrorCode.java b/src/main/java/com/foru/freebe/errors/errorcode/ScheduleErrorCode.java index f326260..a3a65c7 100644 --- a/src/main/java/com/foru/freebe/errors/errorcode/ScheduleErrorCode.java +++ b/src/main/java/com/foru/freebe/errors/errorcode/ScheduleErrorCode.java @@ -9,7 +9,8 @@ public enum ScheduleErrorCode implements ErrorCode { DAILY_SCHEDULE_NOT_FOUND(500, "해당하는 날짜별 스케줄을 찾을 수 없습니다."), DAILY_SCHEDULE_OVERLAP(400, "해당 일정에 이미 등록된 스케줄이 있습니다."), DAILY_SCHEDULE_IN_PAST(400, "현재 시점 이전의 스케줄은 등록할 수 없습니다."), - START_TIME_AFTER_END_TIME(400, "시작시간과 종료시간이 올바르지 않습니다."); + START_TIME_AFTER_END_TIME(400, "시작시간과 종료시간이 올바르지 않습니다."), + INVALID_SCHEDULE_UNIT(400, "기본스케줄 단위와 일치하지 않습니다."); private final int httpStatus; private final String message; diff --git a/src/main/java/com/foru/freebe/member/entity/ScheduleUnit.java b/src/main/java/com/foru/freebe/member/entity/ScheduleUnit.java index c5e9dd2..4dd257c 100644 --- a/src/main/java/com/foru/freebe/member/entity/ScheduleUnit.java +++ b/src/main/java/com/foru/freebe/member/entity/ScheduleUnit.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; public enum ScheduleUnit { - @JsonProperty("THIRTY_MINUTES") THIRTY_MINUTES, @JsonProperty("SIXTY_MINUTES") diff --git a/src/main/java/com/foru/freebe/schedule/repository/DailyScheduleRepository.java b/src/main/java/com/foru/freebe/schedule/repository/DailyScheduleRepository.java index 86a0c84..dce5048 100644 --- a/src/main/java/com/foru/freebe/schedule/repository/DailyScheduleRepository.java +++ b/src/main/java/com/foru/freebe/schedule/repository/DailyScheduleRepository.java @@ -10,6 +10,7 @@ import com.foru.freebe.member.entity.Member; import com.foru.freebe.schedule.entity.DailySchedule; +import com.foru.freebe.schedule.entity.ScheduleStatus; public interface DailyScheduleRepository extends JpaRepository { Optional findByMemberAndId(Member member, Long scheduleId); @@ -18,7 +19,8 @@ public interface DailyScheduleRepository extends JpaRepository :startTime))") - List findOverlappingSchedules(Member photographer, LocalDate date, LocalTime startTime, - LocalTime endTime); + + "AND ((ds.startTime < :endTime AND ds.endTime > :startTime))" + + "AND ds.scheduleStatus IN (:scheduleStatuses)") + List findConflictingSchedulesByStatuses(Member photographer, LocalDate date, LocalTime startTime, + LocalTime endTime, List scheduleStatuses); } diff --git a/src/main/java/com/foru/freebe/schedule/service/BaseScheduleService.java b/src/main/java/com/foru/freebe/schedule/service/BaseScheduleService.java index dac19df..4cc3b55 100644 --- a/src/main/java/com/foru/freebe/schedule/service/BaseScheduleService.java +++ b/src/main/java/com/foru/freebe/schedule/service/BaseScheduleService.java @@ -1,24 +1,23 @@ package com.foru.freebe.schedule.service; import java.time.LocalTime; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.foru.freebe.errors.errorcode.MemberErrorCode; +import com.foru.freebe.errors.errorcode.ScheduleErrorCode; +import com.foru.freebe.errors.exception.RestApiException; +import com.foru.freebe.member.entity.Member; +import com.foru.freebe.member.repository.MemberRepository; import com.foru.freebe.schedule.dto.BaseScheduleDto; import com.foru.freebe.schedule.dto.ScheduleUnitDto; import com.foru.freebe.schedule.entity.BaseSchedule; import com.foru.freebe.schedule.entity.DayOfWeek; import com.foru.freebe.schedule.entity.OperationStatus; import com.foru.freebe.schedule.repository.BaseScheduleRepository; -import com.foru.freebe.errors.errorcode.MemberErrorCode; -import com.foru.freebe.errors.errorcode.ScheduleErrorCode; -import com.foru.freebe.errors.exception.RestApiException; -import com.foru.freebe.member.entity.Member; -import com.foru.freebe.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -62,7 +61,7 @@ public void updateSchedule(BaseScheduleDto baseScheduleDto, Member photographer) } public void createDefaultSchedule(Member photographer) { - for(DayOfWeek dayOfWeek : DayOfWeek.values()) { + for (DayOfWeek dayOfWeek : DayOfWeek.values()) { BaseSchedule baseSchedule = BaseSchedule.builder() .photographer(photographer) .dayOfWeek(dayOfWeek) diff --git a/src/main/java/com/foru/freebe/schedule/service/DailyScheduleService.java b/src/main/java/com/foru/freebe/schedule/service/DailyScheduleService.java index 5fdc7e6..21badb9 100644 --- a/src/main/java/com/foru/freebe/schedule/service/DailyScheduleService.java +++ b/src/main/java/com/foru/freebe/schedule/service/DailyScheduleService.java @@ -1,8 +1,5 @@ package com.foru.freebe.schedule.service; -import java.time.Clock; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.List; import java.util.stream.Collectors; @@ -26,7 +23,7 @@ @Transactional public class DailyScheduleService { private final DailyScheduleRepository dailyScheduleRepository; - private final Clock clock; + private final DailyScheduleValidator validator; public List getDailySchedules(Member photographer, DailyScheduleMonthlyRequest request) { return dailyScheduleRepository.findByMember(photographer) @@ -38,9 +35,10 @@ public List getDailySchedules(Member photographer, DailyS } public DailyScheduleAddResponse addDailySchedule(Member photographer, DailyScheduleRequest request) { - validateTimeRange(request.getStartTime(), request.getEndTime()); - validateScheduleInFuture(request); - validateScheduleOverlap(photographer, request); + validator.validateTimeRange(request.getStartTime(), request.getEndTime()); + validator.validateScheduleUnit(photographer.getScheduleUnit(), request.getStartTime(), request.getEndTime()); + validator.validateScheduleInFuture(request); + validator.validateConflictingSchedules(photographer, request); DailySchedule dailySchedule = DailySchedule.builder() .member(photographer) @@ -55,13 +53,14 @@ public DailyScheduleAddResponse addDailySchedule(Member photographer, DailySched } public void updateDailySchedule(Member photographer, Long scheduleId, DailyScheduleRequest request) { - validateTimeRange(request.getStartTime(), request.getEndTime()); + validator.validateTimeRange(request.getStartTime(), request.getEndTime()); + validator.validateScheduleUnit(photographer.getScheduleUnit(), request.getStartTime(), request.getEndTime()); DailySchedule dailySchedule = dailyScheduleRepository.findByMemberAndId(photographer, scheduleId) .orElseThrow(() -> new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_NOT_FOUND)); - validateScheduleInFuture(request); - validateScheduleOverlap(photographer, request, scheduleId); + validator.validateScheduleInFuture(request); + validator.validateConflictingSchedules(photographer, request, scheduleId); dailySchedule.updateScheduleStatus(request.getScheduleStatus()); dailySchedule.updateDate(request.getDate()); @@ -74,41 +73,6 @@ public void deleteDailySchedule(Member photographer, Long scheduleId) { .orElseThrow(() -> new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_NOT_FOUND))); } - private void validateScheduleOverlap(Member member, DailyScheduleRequest request) { - List overlappingSchedules = dailyScheduleRepository.findOverlappingSchedules(member, - request.getDate(), request.getStartTime(), request.getEndTime()); - - if (!overlappingSchedules.isEmpty()) { - throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP); - } - } - - private void validateTimeRange(LocalTime startTime, LocalTime endTime) { - if (startTime.isAfter(endTime) || startTime.equals(endTime)) { - throw new RestApiException(ScheduleErrorCode.START_TIME_AFTER_END_TIME); - } - } - - private void validateScheduleInFuture(DailyScheduleRequest request) { - LocalDateTime requestDateTime = request.getDate().atTime(request.getStartTime()); - - if (requestDateTime.isBefore(LocalDateTime.now(clock))) { - throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_IN_PAST); - } - } - - private void validateScheduleOverlap(Member member, DailyScheduleRequest request, Long scheduleId) { - List overlappingSchedules = dailyScheduleRepository.findOverlappingSchedules(member, - request.getDate(), request.getStartTime(), request.getEndTime()); - - if (overlappingSchedules.size() == 1 && overlappingSchedules.get(0).getId().equals(scheduleId)) { - return; - } - if (!overlappingSchedules.isEmpty()) { - throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP); - } - } - private DailyScheduleResponse toDailyScheduleResponse(DailySchedule dailySchedule) { return DailyScheduleResponse.builder() .scheduleId(dailySchedule.getId()) diff --git a/src/main/java/com/foru/freebe/schedule/service/DailyScheduleValidator.java b/src/main/java/com/foru/freebe/schedule/service/DailyScheduleValidator.java new file mode 100644 index 0000000..ea79ed5 --- /dev/null +++ b/src/main/java/com/foru/freebe/schedule/service/DailyScheduleValidator.java @@ -0,0 +1,95 @@ +package com.foru.freebe.schedule.service; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.foru.freebe.errors.errorcode.ScheduleErrorCode; +import com.foru.freebe.errors.exception.RestApiException; +import com.foru.freebe.member.entity.Member; +import com.foru.freebe.member.entity.ScheduleUnit; +import com.foru.freebe.schedule.dto.DailyScheduleRequest; +import com.foru.freebe.schedule.entity.DailySchedule; +import com.foru.freebe.schedule.entity.ScheduleStatus; +import com.foru.freebe.schedule.repository.DailyScheduleRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class DailyScheduleValidator { + private final Clock clock; + private final DailyScheduleRepository dailyScheduleRepository; + + public void validateTimeRange(LocalTime startTime, LocalTime endTime) { + if (startTime.isAfter(endTime) || startTime.equals(endTime)) { + throw new RestApiException(ScheduleErrorCode.START_TIME_AFTER_END_TIME); + } + } + + public void validateScheduleUnit(ScheduleUnit scheduleUnit, LocalTime startTime, LocalTime endTime) { + switch (scheduleUnit) { + case SIXTY_MINUTES -> { + if (startTime.getMinute() != 0 || endTime.getMinute() != 0) { + throw new RestApiException(ScheduleErrorCode.INVALID_SCHEDULE_UNIT); + } + } + case THIRTY_MINUTES -> { + if (startTime.getMinute() != 0 && startTime.getMinute() != 30) { + throw new RestApiException(ScheduleErrorCode.INVALID_SCHEDULE_UNIT); + } else if (endTime.getMinute() != 0 && endTime.getMinute() != 30) { + throw new RestApiException(ScheduleErrorCode.INVALID_SCHEDULE_UNIT); + } + } + } + } + + public void validateScheduleInFuture(DailyScheduleRequest request) { + LocalDateTime requestDateTime = request.getDate().atTime(request.getStartTime()); + + if (requestDateTime.isBefore(LocalDateTime.now(clock))) { + throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_IN_PAST); + } + } + + public void validateConflictingSchedules(Member member, DailyScheduleRequest request) { + List scheduleStatuses = determineConflictingStatuses(request.getScheduleStatus()); + + List overlappingSchedules = dailyScheduleRepository.findConflictingSchedulesByStatuses(member, + request.getDate(), request.getStartTime(), request.getEndTime(), scheduleStatuses); + + if (!overlappingSchedules.isEmpty()) { + throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP); + } + } + + public void validateConflictingSchedules(Member member, DailyScheduleRequest request, Long scheduleId) { + List scheduleStatuses = determineConflictingStatuses(request.getScheduleStatus()); + + List conflictingSchedules = dailyScheduleRepository.findConflictingSchedulesByStatuses(member, + request.getDate(), request.getStartTime(), request.getEndTime(), scheduleStatuses); + + if (conflictingSchedules.isEmpty() || isSelfConflictOnly(scheduleId, conflictingSchedules)) { + return; + } + + throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP); + } + + private List determineConflictingStatuses(ScheduleStatus scheduleStatus) { + if (scheduleStatus == ScheduleStatus.CONFIRMED) { + return List.of(ScheduleStatus.CONFIRMED); + } else { + return List.of(ScheduleStatus.OPEN, ScheduleStatus.CLOSED); + } + } + + private boolean isSelfConflictOnly(Long scheduleId, List conflictingSchedules) { + return conflictingSchedules.size() == 1 && conflictingSchedules.get(0).getId().equals(scheduleId); + } +} diff --git a/src/test/java/com/foru/freebe/schedule/service/BaseScheduleServiceTest.java b/src/test/java/com/foru/freebe/schedule/service/BaseScheduleServiceTest.java index e927a98..c46ec62 100644 --- a/src/test/java/com/foru/freebe/schedule/service/BaseScheduleServiceTest.java +++ b/src/test/java/com/foru/freebe/schedule/service/BaseScheduleServiceTest.java @@ -3,6 +3,7 @@ import static org.mockito.Mockito.*; import java.time.LocalTime; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -11,6 +12,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import com.foru.freebe.member.entity.Member; import com.foru.freebe.member.entity.Role; import com.foru.freebe.schedule.dto.BaseScheduleDto; @@ -21,7 +23,6 @@ @ExtendWith(MockitoExtension.class) class BaseScheduleServiceTest { - @Mock private BaseScheduleRepository baseScheduleRepository; @@ -92,10 +93,12 @@ void updateScheduleForActivation() { Assertions.assertEquals(LocalTime.of(9, 0), existingBaseSchedule.getStartTime()); Assertions.assertEquals(LocalTime.of(18, 0), existingBaseSchedule.getEndTime()); - verify(baseScheduleRepository, times(1)).findByDayOfWeekAndPhotographerId(baseScheduleDto.getDayOfWeek(), photographer.getId()); + verify(baseScheduleRepository, times(1)).findByDayOfWeekAndPhotographerId(baseScheduleDto.getDayOfWeek(), + photographer.getId()); } - private BaseScheduleDto createBaseScheduleDto(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime, OperationStatus operationStatus) { + private BaseScheduleDto createBaseScheduleDto(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime, + OperationStatus operationStatus) { return BaseScheduleDto.builder() .dayOfWeek(dayOfWeek) .startTime(startTime) diff --git a/src/test/java/com/foru/freebe/schedule/service/DailyScheduleServiceTest.java b/src/test/java/com/foru/freebe/schedule/service/DailyScheduleServiceTest.java new file mode 100644 index 0000000..454581e --- /dev/null +++ b/src/test/java/com/foru/freebe/schedule/service/DailyScheduleServiceTest.java @@ -0,0 +1,98 @@ +package com.foru.freebe.schedule.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.foru.freebe.member.entity.Member; +import com.foru.freebe.member.entity.Role; +import com.foru.freebe.schedule.dto.DailyScheduleMonthlyRequest; +import com.foru.freebe.schedule.dto.DailyScheduleResponse; +import com.foru.freebe.schedule.entity.DailySchedule; +import com.foru.freebe.schedule.entity.ScheduleStatus; +import com.foru.freebe.schedule.repository.DailyScheduleRepository; + +@ExtendWith(MockitoExtension.class) +public class DailyScheduleServiceTest { + @Mock + private DailyScheduleRepository dailyScheduleRepository; + + @Mock + private Clock clock; + + @InjectMocks + private DailyScheduleService dailyScheduleService; + + private Member photographer; + private LocalDateTime now; + + @BeforeEach + void setUp() { + Instant fixedInstant = Instant.parse("2024-12-05T10:00:00Z"); + ZoneId zoneId = ZoneId.systemDefault(); + + given(clock.instant()).willReturn(fixedInstant); + given(clock.getZone()).willReturn(zoneId); + + now = LocalDateTime.now(clock); + photographer = Member + .builder(1L, Role.PHOTOGRAPHER, "tester", "test@email", "010-0000-0000") + .build(); + photographer.initializeScheduleUnit(); + } + + @Nested + @DisplayName("날짜별 스케줄 조회 테스트") + class FindDailySchedule { + @Test + @DisplayName("날짜별 스케줄 조회 시 월별 데이터를 불러온다") + void shouldFetchSchedulesAfterCurrentDate() { + // given + List dailySchedules = new ArrayList<>(); + DailySchedule dailySchedule1 = DailySchedule.builder() + .member(photographer) + .scheduleStatus(ScheduleStatus.OPEN) + .date(now.toLocalDate()) + .startTime(LocalTime.of(10, 0, 0)) + .endTime(LocalTime.of(11, 0, 0)) + .build(); + DailySchedule dailySchedule2 = DailySchedule.builder() + .member(photographer) + .scheduleStatus(ScheduleStatus.OPEN) + .date(now.toLocalDate().plusMonths(1L)) + .startTime(now.toLocalTime().minusSeconds(1L)) + .endTime(now.toLocalTime()) + .build(); + dailySchedules.add(dailySchedule1); + dailySchedules.add(dailySchedule2); + + DailyScheduleMonthlyRequest request = new DailyScheduleMonthlyRequest( + now.toLocalDate().getYear(), now.toLocalDate().getMonthValue()); + + given(dailyScheduleRepository.findByMember(photographer)).willReturn(dailySchedules); + + // when + List responses = dailyScheduleService.getDailySchedules(photographer, request); + + // then + assertThat(responses).size().isEqualTo(1); + assertThat(responses.get(0).getDate()).isEqualTo(now.toLocalDate()); + } + } +} diff --git a/src/test/java/com/foru/freebe/schedule/DailyScheduleServiceTest.java b/src/test/java/com/foru/freebe/schedule/service/DailyScheduleValidatorTest.java similarity index 52% rename from src/test/java/com/foru/freebe/schedule/DailyScheduleServiceTest.java rename to src/test/java/com/foru/freebe/schedule/service/DailyScheduleValidatorTest.java index 98f6d17..bc5b5b0 100644 --- a/src/test/java/com/foru/freebe/schedule/DailyScheduleServiceTest.java +++ b/src/test/java/com/foru/freebe/schedule/service/DailyScheduleValidatorTest.java @@ -1,4 +1,4 @@ -package com.foru.freebe.schedule; +package com.foru.freebe.schedule.service; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -25,17 +25,14 @@ import com.foru.freebe.errors.exception.RestApiException; import com.foru.freebe.member.entity.Member; import com.foru.freebe.member.entity.Role; -import com.foru.freebe.schedule.dto.DailyScheduleAddResponse; -import com.foru.freebe.schedule.dto.DailyScheduleMonthlyRequest; +import com.foru.freebe.member.entity.ScheduleUnit; import com.foru.freebe.schedule.dto.DailyScheduleRequest; -import com.foru.freebe.schedule.dto.DailyScheduleResponse; import com.foru.freebe.schedule.entity.DailySchedule; import com.foru.freebe.schedule.entity.ScheduleStatus; import com.foru.freebe.schedule.repository.DailyScheduleRepository; -import com.foru.freebe.schedule.service.DailyScheduleService; @ExtendWith(MockitoExtension.class) -public class DailyScheduleServiceTest { +public class DailyScheduleValidatorTest { @Mock private DailyScheduleRepository dailyScheduleRepository; @@ -43,65 +40,65 @@ public class DailyScheduleServiceTest { private Clock clock; @InjectMocks - private DailyScheduleService dailyScheduleService; + private DailyScheduleValidator dailyScheduleValidator; private Member photographer; private LocalDateTime now; @BeforeEach void setUp() { - Instant fixedInstant = Instant.parse("2024-12-05T10:00:00Z"); + Instant fixedInstant = Instant.parse("2025-01-22T10:00:00Z"); ZoneId zoneId = ZoneId.systemDefault(); given(clock.instant()).willReturn(fixedInstant); given(clock.getZone()).willReturn(zoneId); now = LocalDateTime.now(clock); - photographer = Member.builder(1L, Role.PHOTOGRAPHER, "tester", "test@email", "010-0000-0000").build(); + photographer = Member + .builder(1L, Role.PHOTOGRAPHER, "tester", "test@email", "010-0000-0000") + .build(); + photographer.initializeScheduleUnit(); } @Nested - @DisplayName("날짜별 스케줄 조회 테스트") - class FindDailySchedule { + @DisplayName("날짜별 스케줄과 기본 스케줄의 단위 일치성 보장 테스트") + class ValidateDailyScheduleUnit { @Test - @DisplayName("날짜별 스케줄 조회 시 월별 데이터를 불러온다") - void shouldFetchSchedulesAfterCurrentDate() { + @DisplayName("기본 스케줄 단위가 60분일 때, 시작시간과 종료시간이 정시가 아니면 예외가 발생한다") + void shouldThrowExceptionForInvalidTime() { // given - List dailySchedules = new ArrayList<>(); - DailySchedule dailySchedule1 = DailySchedule.builder() - .member(photographer) - .scheduleStatus(ScheduleStatus.OPEN) - .date(now.toLocalDate()) - .startTime(LocalTime.of(10, 0, 0)) - .endTime(LocalTime.of(11, 0, 0)) - .build(); - DailySchedule dailySchedule2 = DailySchedule.builder() - .member(photographer) - .scheduleStatus(ScheduleStatus.OPEN) - .date(now.toLocalDate().plusMonths(1L)) - .startTime(now.toLocalTime().minusSeconds(1L)) - .endTime(now.toLocalTime()) - .build(); - dailySchedules.add(dailySchedule1); - dailySchedules.add(dailySchedule2); + ScheduleUnit basicScheduleUnit = ScheduleUnit.SIXTY_MINUTES; + LocalTime invalidStartTime = LocalTime.parse("10:30:00"); + LocalTime invalidEndTime = LocalTime.parse("11:30:00"); - DailyScheduleMonthlyRequest request = new DailyScheduleMonthlyRequest( - now.toLocalDate().getYear(), now.toLocalDate().getMonthValue()); + // when & then + RestApiException exception = assertThrows(RestApiException.class, () -> { + dailyScheduleValidator.validateScheduleUnit(basicScheduleUnit, invalidStartTime, invalidEndTime); + }); - given(dailyScheduleRepository.findByMember(photographer)).willReturn(dailySchedules); + assertThat(exception.getErrorCode()).isEqualTo(ScheduleErrorCode.INVALID_SCHEDULE_UNIT); + } - // when - List responses = dailyScheduleService.getDailySchedules(photographer, request); + @Test + @DisplayName("기본 스케줄 단위와 날짜별 스케줄 단위가 일치하지 않으면 예외가 발생한다") + void shouldThrowExceptionForInvalidUnit() { + // given + ScheduleUnit basicScheduleUnit = ScheduleUnit.SIXTY_MINUTES; + LocalTime invalidStartTime = LocalTime.parse("10:00:00"); + LocalTime invalidEndTime = LocalTime.parse("11:30:00"); + + // when & then + RestApiException exception = assertThrows(RestApiException.class, () -> { + dailyScheduleValidator.validateScheduleUnit(basicScheduleUnit, invalidStartTime, invalidEndTime); + }); - // then - assertThat(responses).size().isEqualTo(1); - assertThat(responses.get(0).getDate()).isEqualTo(now.toLocalDate()); + assertThat(exception.getErrorCode()).isEqualTo(ScheduleErrorCode.INVALID_SCHEDULE_UNIT); } } @Nested - @DisplayName("날짜별 스케줄 추가 테스트") - class AddDailySchedule { + @DisplayName("날짜별 스케줄 요청 객체의 유효성 검증 테스트") + class ValidateDailyScheduleRequestDTO { @Test @DisplayName("현시점 이전의 스케줄은 등록할 수 없다") void shouldNotAllowSchedulesInThePast() { @@ -115,15 +112,19 @@ void shouldNotAllowSchedulesInThePast() { //when & then RestApiException exception = assertThrows(RestApiException.class, () -> { - dailyScheduleService.addDailySchedule(photographer, request); + dailyScheduleValidator.validateScheduleInFuture(request); }); assertThat(exception.getErrorCode()).isEqualTo(ScheduleErrorCode.DAILY_SCHEDULE_IN_PAST); } + } + @Nested + @DisplayName("날짜별 스케줄 간 충돌 테스트") + class ValidateDailyScheduleConflict { @Test - @DisplayName("중복되는 스케줄이 있을 때 예외가 발생한다") - void shouldThrowExceptionWhenSchedulesOverlap() { + @DisplayName("예약오픈, 예약중지 간 중복되는 스케줄이 있을 때 예외가 발생한다") + void shouldThrowExceptionWhenSchedulesConflict() { // given DailyScheduleRequest request = DailyScheduleRequest.builder() .scheduleStatus(ScheduleStatus.OPEN) @@ -135,19 +136,19 @@ void shouldThrowExceptionWhenSchedulesOverlap() { List overlappingSchedules = new ArrayList<>(); overlappingSchedules.add(DailySchedule.builder() .member(photographer) - .scheduleStatus(ScheduleStatus.OPEN) + .scheduleStatus(ScheduleStatus.CLOSED) .date(now.toLocalDate()) .startTime(now.toLocalTime().plusHours(1).plusMinutes(30)) .endTime(now.toLocalTime().plusHours(3)) .build()); - given(dailyScheduleRepository.findOverlappingSchedules(photographer, request.getDate(), - request.getStartTime(), request.getEndTime())) + given(dailyScheduleRepository.findConflictingSchedulesByStatuses(photographer, request.getDate(), + request.getStartTime(), request.getEndTime(), List.of(ScheduleStatus.OPEN, ScheduleStatus.CLOSED))) .willReturn(overlappingSchedules); // when & then RestApiException exception = assertThrows(RestApiException.class, () -> { - dailyScheduleService.addDailySchedule(photographer, request); + dailyScheduleValidator.validateConflictingSchedules(photographer, request); }); assertThat(exception.getErrorCode()).isEqualTo(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP); @@ -155,7 +156,7 @@ void shouldThrowExceptionWhenSchedulesOverlap() { @Test @DisplayName("중복되는 스케줄이 없으면 예외가 발생하지 않는다") - void shouldNotThrowExceptionWhenNoOverlappingSchedules() { + void shouldNotThrowExceptionWhenNoConflictingSchedules() { // given DailyScheduleRequest request = DailyScheduleRequest.builder() .scheduleStatus(ScheduleStatus.OPEN) @@ -166,25 +167,12 @@ void shouldNotThrowExceptionWhenNoOverlappingSchedules() { List overlappingSchedules = new ArrayList<>(); - given(dailyScheduleRepository.findOverlappingSchedules(photographer, request.getDate(), - request.getStartTime(), request.getEndTime())) + given(dailyScheduleRepository.findConflictingSchedulesByStatuses(photographer, request.getDate(), + request.getStartTime(), request.getEndTime(), List.of(ScheduleStatus.OPEN, ScheduleStatus.CLOSED))) .willReturn(overlappingSchedules); - given(dailyScheduleRepository.save(any(DailySchedule.class))).willReturn( - DailySchedule.builder() - .member(photographer) - .scheduleStatus(ScheduleStatus.OPEN) - .date(now.toLocalDate()) - .startTime(now.toLocalTime().plusHours(1)) - .endTime(now.toLocalTime().plusHours(2)) - .build() - ); - - // when - DailyScheduleAddResponse response = dailyScheduleService.addDailySchedule(photographer, request); - - // then - assertThat(response).isNotNull(); + // when & then + assertDoesNotThrow(() -> dailyScheduleValidator.validateConflictingSchedules(photographer, request)); } } }