From eb54cc160cf4f3dcc841b2f931167766a2655a6c Mon Sep 17 00:00:00 2001 From: dnjswldnd-3513 Date: Fri, 18 Sep 2026 10:04:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=83=9D=ED=98=91=20=ED=95=99?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=90=EB=8F=99=20=EC=A0=84=ED=99=98=20=EC=8B=9C?= =?UTF-8?q?=20=EC=98=A4=EB=8A=98=EC=9D=B4=20=ED=8F=AC=ED=95=A8=EB=90=98?= =?UTF-8?q?=EA=B3=A0=20=EB=A7=A4=EC=9E=A5=EC=9D=B4=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=ED=95=99=EA=B8=B0=EB=A5=BC=20=EC=84=A0=ED=83=9D=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 --- .../domain/coopshop/model/CoopSemester.java | 8 + .../repository/CoopSemesterRepository.java | 16 +- .../coopshop/service/CoopShopService.java | 21 ++- .../acceptance/domain/CoopShopApiTest.java | 21 +++ .../fixture/CoopShopAcceptanceFixture.java | 10 + .../coopshop/service/CoopShopServiceTest.java | 175 ++++++++++++++++++ 6 files changed, 237 insertions(+), 14 deletions(-) create mode 100644 src/test/java/in/koreatech/koin/unit/domain/coopshop/service/CoopShopServiceTest.java 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 4595779775..30fa81f231 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.DayOfWeek; @@ -15,7 +16,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; @@ -28,6 +28,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 @@ -99,20 +100,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..49403f7361 --- /dev/null +++ b/src/test/java/in/koreatech/koin/unit/domain/coopshop/service/CoopShopServiceTest.java @@ -0,0 +1,175 @@ +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.CoopOpenRepository; +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 CoopOpenRepository coopOpenRepository; + + @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, + coopOpenRepository, + 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; + } +} From 456ce402dda18a2a8221432867a0626a2131faf7 Mon Sep 17 00:00:00 2001 From: dnjswldnd-3513 Date: Thu, 29 Oct 2026 13:59:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=EC=83=9D=ED=98=91=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=9C=EA=B1=B0=EB=90=9C=20CoopOpenRepository=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/domain/coopshop/service/CoopShopServiceTest.java | 5 ----- 1 file changed, 5 deletions(-) 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 index 49403f7361..77adebfaa2 100644 --- 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 @@ -22,7 +22,6 @@ 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.CoopOpenRepository; import in.koreatech.koin.domain.coopshop.repository.CoopSemesterRepository; import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; import in.koreatech.koin.domain.coopshop.service.CoopShopService; @@ -38,9 +37,6 @@ class CoopShopServiceTest { @Mock private CoopShopRepository coopShopRepository; - @Mock - private CoopOpenRepository coopOpenRepository; - @Mock private CoopSemesterRepository coopSemesterRepository; @@ -55,7 +51,6 @@ void setUp() { coopShopService = new CoopShopService( clock, coopShopRepository, - coopOpenRepository, coopSemesterRepository, coopNameRepository );