diff --git a/src/main/java/com/boaz/backend/domain/recruitment/repository/RecruitmentRepository.java b/src/main/java/com/boaz/backend/domain/recruitment/repository/RecruitmentRepository.java index cd3d728..8f12275 100644 --- a/src/main/java/com/boaz/backend/domain/recruitment/repository/RecruitmentRepository.java +++ b/src/main/java/com/boaz/backend/domain/recruitment/repository/RecruitmentRepository.java @@ -14,6 +14,10 @@ public interface RecruitmentRepository extends JpaRepository @Query("SELECT r FROM Recruitment r WHERE r.startDate <= :now AND r.endDate >= :now") Optional findActiveRecruitment(LocalDateTime now); + // 진행 중이거나 아직 마감되지 않은 공고 중 가장 임박한 것 (예정 포함) + @Query("SELECT r FROM Recruitment r WHERE r.endDate >= :now ORDER BY r.startDate ASC LIMIT 1") + Optional findCurrentOrUpcoming(LocalDateTime now); + // 기수로 공고 조회 Optional findByTerm(Integer term); diff --git a/src/main/java/com/boaz/backend/domain/recruitment/service/RecruitmentService.java b/src/main/java/com/boaz/backend/domain/recruitment/service/RecruitmentService.java index f36d73f..49c8642 100644 --- a/src/main/java/com/boaz/backend/domain/recruitment/service/RecruitmentService.java +++ b/src/main/java/com/boaz/backend/domain/recruitment/service/RecruitmentService.java @@ -92,11 +92,9 @@ private LocalDateTime now() { // 모집 중 여부 조회 public RecruitmentStatusResponse getRecruitmentStatus() { - Optional activeRecruitment = recruitmentRepository - .findActiveRecruitment(now()); - - return activeRecruitment - .map(r -> RecruitmentStatusResponse.of(true, r.getTerm())) + LocalDateTime now = now(); + return recruitmentRepository.findCurrentOrUpcoming(now) + .map(r -> RecruitmentStatusResponse.of(r.isActive(now), r.getTerm())) .orElse(RecruitmentStatusResponse.of(false, null)); } @@ -113,9 +111,7 @@ public RecruitmentResponse getRecruitment(Integer term) { Recruitment recruitment = recruitmentRepository.findByTerm(term) .orElseThrow(() -> new CustomException(ErrorCode.RECRUITMENT_NOT_FOUND)); - boolean isActive = recruitment.isActive(now()); - - return RecruitmentResponse.from(recruitment, isActive); + return RecruitmentResponse.from(recruitment, now()); } // 지원서 질문 조회하기 diff --git a/src/test/java/com/boaz/backend/domain/recruitment/service/RecruitmentServiceTest.java b/src/test/java/com/boaz/backend/domain/recruitment/service/RecruitmentServiceTest.java index 2503ab8..21020a7 100644 --- a/src/test/java/com/boaz/backend/domain/recruitment/service/RecruitmentServiceTest.java +++ b/src/test/java/com/boaz/backend/domain/recruitment/service/RecruitmentServiceTest.java @@ -84,6 +84,14 @@ private Recruitment createExpiredRecruitment() { return r; } + private Recruitment createUpcomingRecruitment() { + Recruitment r = Recruitment.create(28, + LocalDateTime.now().plusDays(14), LocalDateTime.now().plusDays(30), + "[]", null); + ReflectionTestUtils.setField(r, "id", 3L); + return r; + } + private User createUser(Long id) { User u = User.builder() .provider("kakao").providerId("test-" + id) @@ -197,7 +205,7 @@ class GetRecruitmentStatus { @DisplayName("TC-001 모집 기간 중 → isActive:true, term 반환") void active() { Recruitment r = createActiveRecruitment(); - when(recruitmentRepository.findActiveRecruitment(any())).thenReturn(Optional.of(r)); + when(recruitmentRepository.findCurrentOrUpcoming(any())).thenReturn(Optional.of(r)); RecruitmentStatusResponse res = recruitmentService.getRecruitmentStatus(); @@ -206,9 +214,9 @@ void active() { } @Test - @DisplayName("TC-002 모집 기간 외 공고만 있을 때 → isActive:false") + @DisplayName("TC-002 마감된 공고만 존재 (endDate 경과) → isActive:false, term:null") void expiredRecruitment() { - when(recruitmentRepository.findActiveRecruitment(any())).thenReturn(Optional.empty()); + when(recruitmentRepository.findCurrentOrUpcoming(any())).thenReturn(Optional.empty()); RecruitmentStatusResponse res = recruitmentService.getRecruitmentStatus(); @@ -219,7 +227,7 @@ void expiredRecruitment() { @Test @DisplayName("TC-003 공고 데이터 없음 → isActive:false (예외 없음)") void noData() { - when(recruitmentRepository.findActiveRecruitment(any())).thenReturn(Optional.empty()); + when(recruitmentRepository.findCurrentOrUpcoming(any())).thenReturn(Optional.empty()); RecruitmentStatusResponse res = recruitmentService.getRecruitmentStatus(); @@ -229,10 +237,13 @@ void noData() { @Test @DisplayName("TC-004 start_date 정각 → isActive:true") void startDateBoundary() { - LocalDateTime now = LocalDateTime.now(); - Recruitment r = Recruitment.create(27, now, now.plusDays(7), "[]", null); + LocalDateTime fixedNow = LocalDateTime.of(2026, 7, 1, 10, 0, 0); + Clock fixed = Clock.fixed(fixedNow.atZone(ZoneId.of("Asia/Seoul")).toInstant(), ZoneId.of("Asia/Seoul")); + ReflectionTestUtils.setField(recruitmentService, "clock", fixed); + + Recruitment r = Recruitment.create(27, fixedNow, fixedNow.plusDays(7), "[]", null); ReflectionTestUtils.setField(r, "id", 1L); - when(recruitmentRepository.findActiveRecruitment(any())).thenReturn(Optional.of(r)); + when(recruitmentRepository.findCurrentOrUpcoming(any())).thenReturn(Optional.of(r)); assertThat(recruitmentService.getRecruitmentStatus().getIsActive()).isTrue(); } @@ -240,10 +251,22 @@ void startDateBoundary() { @Test @DisplayName("TC-005 end_date 1초 초과 → isActive:false") void endDatePast() { - when(recruitmentRepository.findActiveRecruitment(any())).thenReturn(Optional.empty()); + when(recruitmentRepository.findCurrentOrUpcoming(any())).thenReturn(Optional.empty()); assertThat(recruitmentService.getRecruitmentStatus().getIsActive()).isFalse(); } + + @Test + @DisplayName("TC-006 예정 공고 존재 (start_date 미래) → isActive:false, term 반환") + void upcomingRecruitment() { + Recruitment r = createUpcomingRecruitment(); + when(recruitmentRepository.findCurrentOrUpcoming(any())).thenReturn(Optional.of(r)); + + RecruitmentStatusResponse res = recruitmentService.getRecruitmentStatus(); + + assertThat(res.getIsActive()).isFalse(); + assertThat(res.getTerm()).isEqualTo(28); + } } // ══════════════════════════════════════════════ @@ -286,6 +309,21 @@ void notFound() { .extracting(e -> ((CustomException) e).getErrorCode()) .isEqualTo(ErrorCode.RECRUITMENT_NOT_FOUND); } + + @Test + @DisplayName("TC-004 예정 공고 조회 → isActive:false + 날짜/일정 필드 반환 (null 아님)") + void upcomingRecruitment() { + Recruitment r = createUpcomingRecruitment(); + when(recruitmentRepository.findByTerm(28)).thenReturn(Optional.of(r)); + + RecruitmentResponse res = recruitmentService.getRecruitment(28); + + assertThat(res.getIsActive()).isFalse(); + assertThat(res.getTerm()).isEqualTo(28); + assertThat(res.getStartDate()).isNotNull(); + assertThat(res.getEndDate()).isNotNull(); + assertThat(res.getSchedule()).isNotNull(); + } } // ══════════════════════════════════════════════