diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopSemester.java b/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopSemester.java index 574028b3db..a242d80383 100644 --- a/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopSemester.java +++ b/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopSemester.java @@ -94,6 +94,14 @@ public void updateApply(boolean isApplied) { this.isApplied = isApplied; } + public boolean isValidOn(LocalDate date) { + return !date.isBefore(fromDate) && !date.isAfter(toDate); + } + + public boolean hasCoopShops() { + return !coopShops.isEmpty(); + } + public void replaceCoopShops(List coopShops) { this.coopShops.clear(); this.coopShops.addAll(coopShops); diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/repository/CoopSemesterRepository.java b/src/main/java/in/koreatech/koin/domain/coopshop/repository/CoopSemesterRepository.java index e26365c538..a439147bdc 100644 --- a/src/main/java/in/koreatech/koin/domain/coopshop/repository/CoopSemesterRepository.java +++ b/src/main/java/in/koreatech/koin/domain/coopshop/repository/CoopSemesterRepository.java @@ -1,8 +1,12 @@ package in.koreatech.koin.domain.coopshop.repository; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; import in.koreatech.koin.domain.coopshop.exception.CoopSemesterNotFoundException; import in.koreatech.koin.domain.coopshop.model.CoopSemester; @@ -18,10 +22,10 @@ default CoopSemester getByIsApplied(boolean isApplied) { .orElseThrow(() -> CoopSemesterNotFoundException.withDetail("")); } - Optional findTopByOrderByToDateDesc(); - - default CoopSemester getTopByOrderByToDateDesc() { - return findTopByOrderByToDateDesc() - .orElseThrow(() -> CoopSemesterNotFoundException.withDetail("")); - } + @Query(""" + SELECT coopSemester FROM CoopSemester coopSemester + WHERE coopSemester.fromDate <= :date AND coopSemester.toDate >= :date + ORDER BY coopSemester.fromDate DESC, coopSemester.id DESC + """) + List findAllValidOn(@Param("date") LocalDate date); } diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java b/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java index 4aaa3d6229..feaf6b54b1 100644 --- a/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java +++ b/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java @@ -1,6 +1,7 @@ package in.koreatech.koin.domain.coopshop.service; import static in.koreatech.koin.domain.dining.model.DiningType.*; +import static in.koreatech.koin.global.code.ApiResponseCode.NOT_FOUND_COOP_SEMESTER; import java.time.Clock; import java.time.LocalDate; @@ -14,7 +15,6 @@ import in.koreatech.koin.domain.coopshop.dto.CoopShopResponse; import in.koreatech.koin.domain.coopshop.dto.CoopShopsResponse; -import in.koreatech.koin.domain.coopshop.exception.CoopSemesterNotFoundException; import in.koreatech.koin.domain.coopshop.exception.DiningTypeNotFoundException; import in.koreatech.koin.domain.coopshop.model.CoopName; import in.koreatech.koin.domain.coopshop.model.CoopOpen; @@ -25,6 +25,7 @@ import in.koreatech.koin.domain.coopshop.repository.CoopSemesterRepository; import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; import in.koreatech.koin.domain.dining.model.DiningType; +import in.koreatech.koin.global.exception.CustomException; import lombok.RequiredArgsConstructor; @Service @@ -98,20 +99,24 @@ public DiningType getDiningType() { @Transactional public void updateSemester() { CoopSemester currentSemester = coopSemesterRepository.getByIsApplied(true); - if (validateSemester(currentSemester)) { + if (validateSemester(currentSemester) && currentSemester.hasCoopShops()) { return; } + CoopSemester nextSemester = findNextSemester(); currentSemester.updateApply(false); - CoopSemester nextSemester = coopSemesterRepository.getTopByOrderByToDateDesc(); - if (!validateSemester(nextSemester)) { - throw CoopSemesterNotFoundException.withDetail(""); - } nextSemester.updateApply(true); } - public boolean validateSemester(CoopSemester coopSemester) { + private CoopSemester findNextSemester() { LocalDate today = LocalDate.now(clock); - return today.isAfter(coopSemester.getFromDate()) && today.isBefore(coopSemester.getToDate()); + return coopSemesterRepository.findAllValidOn(today).stream() + .filter(CoopSemester::hasCoopShops) + .findFirst() + .orElseThrow(() -> CustomException.of(NOT_FOUND_COOP_SEMESTER, "date : " + today)); + } + + public boolean validateSemester(CoopSemester coopSemester) { + return coopSemester.isValidOn(LocalDate.now(clock)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/CoopShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/CoopShopApiTest.java index d1242c3662..b9817a084b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/CoopShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/CoopShopApiTest.java @@ -131,4 +131,25 @@ void setUp() { } """)); } + + @Test + void 매장_데이터가_없는_미래_학기가_있어도_현재_유효한_학기로_전환된다() throws Exception { + coopShopFixture._23_겨울학기(); + coopShopFixture._24_1학기_매장_없음(); + + coopShopService.updateSemester(); + + mockMvc.perform( + get("/coopshop") + .contentType(MediaType.ALL.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "semester": "23-겨울학기", + "from_date": "2023-12-21", + "to_date": "2024-02-28" + } + """)); + } } diff --git a/src/test/java/in/koreatech/koin/acceptance/fixture/CoopShopAcceptanceFixture.java b/src/test/java/in/koreatech/koin/acceptance/fixture/CoopShopAcceptanceFixture.java index a5bf2bec06..6bb58befa5 100644 --- a/src/test/java/in/koreatech/koin/acceptance/fixture/CoopShopAcceptanceFixture.java +++ b/src/test/java/in/koreatech/koin/acceptance/fixture/CoopShopAcceptanceFixture.java @@ -107,6 +107,16 @@ public CoopShopAcceptanceFixture( return coopSemesterRepository.save(coopSemester); } + public CoopSemester _24_1학기_매장_없음() { + return coopSemesterRepository.save( + CoopSemester.builder() + .semester("24-1학기") + .fromDate(LocalDate.of(2024, 3, 1)) + .toDate(LocalDate.of(2024, 6, 30)) + .build() + ); + } + public CoopSemester 현재학기() { var coopSemester = coopSemesterRepository.save( CoopSemester.builder() diff --git a/src/test/java/in/koreatech/koin/unit/domain/coopshop/service/CoopShopServiceTest.java b/src/test/java/in/koreatech/koin/unit/domain/coopshop/service/CoopShopServiceTest.java new file mode 100644 index 0000000000..77adebfaa2 --- /dev/null +++ b/src/test/java/in/koreatech/koin/unit/domain/coopshop/service/CoopShopServiceTest.java @@ -0,0 +1,170 @@ +package in.koreatech.koin.unit.domain.coopshop.service; + +import static in.koreatech.koin.global.code.ApiResponseCode.NOT_FOUND_COOP_SEMESTER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import in.koreatech.koin.domain.coopshop.model.CoopSemester; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import in.koreatech.koin.domain.coopshop.repository.CoopNameRepository; +import in.koreatech.koin.domain.coopshop.repository.CoopSemesterRepository; +import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; +import in.koreatech.koin.domain.coopshop.service.CoopShopService; +import in.koreatech.koin.global.exception.CustomException; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("NonAsciiCharacters") +class CoopShopServiceTest { + + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + private static final LocalDate TODAY = LocalDate.of(2024, 1, 15); + + @Mock + private CoopShopRepository coopShopRepository; + + @Mock + private CoopSemesterRepository coopSemesterRepository; + + @Mock + private CoopNameRepository coopNameRepository; + + private CoopShopService coopShopService; + + @BeforeEach + void setUp() { + Clock clock = Clock.fixed(TODAY.atStartOfDay(KST).toInstant(), KST); + coopShopService = new CoopShopService( + clock, + coopShopRepository, + coopSemesterRepository, + coopNameRepository + ); + } + + @Test + void 현재_학기가_유효하면_학기를_전환하지_않는다() { + CoopSemester currentSemester = 매장이_있는_학기( + "23-겨울학기", LocalDate.of(2023, 12, 21), LocalDate.of(2024, 2, 28) + ); + currentSemester.updateApply(true); + when(coopSemesterRepository.getByIsApplied(true)).thenReturn(currentSemester); + + coopShopService.updateSemester(); + + assertThat(currentSemester.isApplied()).isTrue(); + verify(coopSemesterRepository, never()).findAllValidOn(any()); + } + + @Test + void 현재_학기가_날짜는_유효해도_매장이_없으면_다른_학기를_찾는다() { + CoopSemester currentEmptySemester = CoopSemester.of( + "23-겨울학기", LocalDate.of(2023, 12, 21), LocalDate.of(2024, 2, 28) + ); + currentEmptySemester.updateApply(true); + CoopSemester overlappingServingSemester = 매장이_있는_학기( + "24-1학기", LocalDate.of(2024, 1, 1), LocalDate.of(2024, 6, 30) + ); + when(coopSemesterRepository.getByIsApplied(true)).thenReturn(currentEmptySemester); + when(coopSemesterRepository.findAllValidOn(TODAY)) + .thenReturn(List.of(overlappingServingSemester, currentEmptySemester)); + + coopShopService.updateSemester(); + + assertThat(currentEmptySemester.isApplied()).isFalse(); + assertThat(overlappingServingSemester.isApplied()).isTrue(); + } + + @Test + void 현재_학기가_날짜는_유효해도_매장이_없고_대체할_학기도_없으면_예외가_발생한다() { + CoopSemester currentEmptySemester = CoopSemester.of( + "23-겨울학기", LocalDate.of(2023, 12, 21), LocalDate.of(2024, 2, 28) + ); + currentEmptySemester.updateApply(true); + when(coopSemesterRepository.getByIsApplied(true)).thenReturn(currentEmptySemester); + when(coopSemesterRepository.findAllValidOn(TODAY)).thenReturn(List.of(currentEmptySemester)); + + assertThatThrownBy(() -> coopShopService.updateSemester()) + .isInstanceOf(CustomException.class) + .hasMessage(NOT_FOUND_COOP_SEMESTER.getMessage()); + assertThat(currentEmptySemester.isApplied()).isTrue(); + } + + @Test + void 만료된_경우_오늘이_포함되고_매장이_있는_학기로_전환한다() { + CoopSemester expiredSemester = 매장이_있는_학기( + "23-2학기", LocalDate.of(2023, 9, 2), LocalDate.of(2023, 12, 20) + ); + expiredSemester.updateApply(true); + CoopSemester emptySemester = CoopSemester.of( + "24-1학기", LocalDate.of(2024, 1, 1), LocalDate.of(2024, 6, 30) + ); + CoopSemester servingSemester = 매장이_있는_학기( + "23-겨울학기", LocalDate.of(2023, 12, 21), LocalDate.of(2024, 2, 28) + ); + when(coopSemesterRepository.getByIsApplied(true)).thenReturn(expiredSemester); + when(coopSemesterRepository.findAllValidOn(TODAY)).thenReturn(List.of(emptySemester, servingSemester)); + + coopShopService.updateSemester(); + + assertThat(expiredSemester.isApplied()).isFalse(); + assertThat(emptySemester.isApplied()).isFalse(); + assertThat(servingSemester.isApplied()).isTrue(); + } + + @Test + void 오늘이_포함되고_매장이_있는_학기가_없으면_예외가_발생한다() { + CoopSemester expiredSemester = 매장이_있는_학기( + "23-2학기", LocalDate.of(2023, 9, 2), LocalDate.of(2023, 12, 20) + ); + expiredSemester.updateApply(true); + CoopSemester emptySemester = CoopSemester.of( + "24-1학기", LocalDate.of(2024, 1, 1), LocalDate.of(2024, 6, 30) + ); + when(coopSemesterRepository.getByIsApplied(true)).thenReturn(expiredSemester); + when(coopSemesterRepository.findAllValidOn(TODAY)).thenReturn(List.of(emptySemester)); + + assertThatThrownBy(() -> coopShopService.updateSemester()) + .isInstanceOf(CustomException.class) + .hasMessage(NOT_FOUND_COOP_SEMESTER.getMessage()); + assertThat(expiredSemester.isApplied()).isTrue(); + } + + @Test + void 학기_시작일과_종료일_당일도_유효한_학기로_판단한다() { + CoopSemester startsToday = CoopSemester.of("24-1학기", TODAY, LocalDate.of(2024, 6, 30)); + CoopSemester endsToday = CoopSemester.of("23-겨울학기", LocalDate.of(2023, 12, 21), TODAY); + CoopSemester endedYesterday = CoopSemester.of( + "23-2학기", LocalDate.of(2023, 9, 2), TODAY.minusDays(1) + ); + + assertThat(coopShopService.validateSemester(startsToday)).isTrue(); + assertThat(coopShopService.validateSemester(endsToday)).isTrue(); + assertThat(coopShopService.validateSemester(endedYesterday)).isFalse(); + } + + private CoopSemester 매장이_있는_학기(String semester, LocalDate fromDate, LocalDate toDate) { + CoopSemester coopSemester = CoopSemester.of(semester, fromDate, toDate); + coopSemester.replaceCoopShops(List.of( + CoopShop.builder() + .location("학생회관 1층") + .phone("041-000-0000") + .build() + )); + return coopSemester; + } +}