diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java b/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java index 7a60a2a80..f779b7324 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java @@ -22,4 +22,10 @@ public static ApplicationPassResponseDto toDto(Application application) { .isPass(application.getIsPass()) .build(); } + + public static ApplicationPassResponseDto defaultResponse() { + return ApplicationPassResponseDto.builder() + .isPass(false) + .build(); + } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/exception/RecruitmentEndDateExceededException.java b/src/main/java/page/clab/api/domain/hiring/application/application/exception/RecruitmentEndDateExceededException.java new file mode 100644 index 000000000..48fd794b7 --- /dev/null +++ b/src/main/java/page/clab/api/domain/hiring/application/application/exception/RecruitmentEndDateExceededException.java @@ -0,0 +1,8 @@ +package page.clab.api.domain.hiring.application.application.exception; + +public class RecruitmentEndDateExceededException extends RuntimeException { + + public RecruitmentEndDateExceededException(String message) { + super(message); + } +} diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java index 7ee116e1a..348aa6df4 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java @@ -7,23 +7,26 @@ import page.clab.api.domain.hiring.application.application.dto.response.ApplicationPassResponseDto; import page.clab.api.domain.hiring.application.application.port.in.CheckApplicationPassStatusUseCase; import page.clab.api.domain.hiring.application.application.port.out.RetrieveApplicationPort; +import page.clab.api.domain.hiring.recruitment.application.port.out.RetrieveRecruitmentPort; +import page.clab.api.domain.hiring.recruitment.domain.Recruitment; @Service @RequiredArgsConstructor public class ApplicationPassCheckService implements CheckApplicationPassStatusUseCase { private final RetrieveApplicationPort retrieveApplicationPort; + private final RetrieveRecruitmentPort retrieveRecruitmentPort; @Transactional(readOnly = true) @Override public ApplicationPassResponseDto checkPassStatus(Long recruitmentId, String studentId) { ApplicationId id = ApplicationId.create(studentId, recruitmentId); + Recruitment recruitment = retrieveRecruitmentPort.findByIdOrThrow(recruitmentId); + + recruitment.validateEndDateWithin7Days(); + return retrieveApplicationPort.findById(id) .map(ApplicationPassResponseDto::toDto) - .orElseGet(() -> - ApplicationPassResponseDto.builder() - .isPass(false) - .build() - ); + .orElseGet(ApplicationPassResponseDto::defaultResponse); } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/in/web/RecentRecruitmentsRetrievalController.java b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/in/web/RecentRecruitmentsRetrievalController.java index 339c426fc..b7609def2 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/in/web/RecentRecruitmentsRetrievalController.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/in/web/RecentRecruitmentsRetrievalController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentEndDateResponseDto; import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentResponseDto; import page.clab.api.domain.hiring.recruitment.application.port.in.RetrieveRecentRecruitmentsUseCase; import page.clab.api.global.common.dto.ApiResponse; @@ -27,4 +28,11 @@ public ApiResponse> retrieveRecentRecruitments() { List recruitments = retrieveRecentRecruitmentsUseCase.retrieveRecentRecruitments(); return ApiResponse.success(recruitments); } + + @Operation(summary = "모집 종료 기간 기준 최신 일주일 모집 공고 목록", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") + @GetMapping("/recent-week") + public ApiResponse> retrieveRecruitmentsByEndDate() { + List recruitments = retrieveRecentRecruitmentsUseCase.retrieveRecruitmentsByEndDate(); + return ApiResponse.success(recruitments); + } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java index be4c978b4..d2025bd4e 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java @@ -8,6 +8,7 @@ import page.clab.api.domain.hiring.recruitment.domain.Recruitment; import page.clab.api.global.exception.NotFoundException; +import java.time.LocalDateTime; import java.util.List; @Component @@ -56,9 +57,9 @@ public List findTop5ByOrderByCreatedAtDesc() { } @Override - public void existsByIdOrThrow(Long recruitmentId) { - if (!repository.existsById(recruitmentId)) { - throw new NotFoundException("[Recruitment] id: " + recruitmentId + "에 해당하는 모집 공고가 존재하지 않습니다."); - } + public List findByEndDateBetween(LocalDateTime weekAgo, LocalDateTime now) { + return repository.findByEndDateBetween(weekAgo, now).stream() + .map(mapper::toDomainEntity) + .toList(); } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentRepository.java b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentRepository.java index dc76a9f15..0ceb88bd7 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentRepository.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentRepository.java @@ -3,9 +3,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.List; @Repository public interface RecruitmentRepository extends JpaRepository { + List findTop5ByOrderByCreatedAtDesc(); + + List findByEndDateBetween(LocalDateTime weekAgo, LocalDateTime now); } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentEndDateResponseDto.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentEndDateResponseDto.java new file mode 100644 index 000000000..c01f86d37 --- /dev/null +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentEndDateResponseDto.java @@ -0,0 +1,29 @@ +package page.clab.api.domain.hiring.recruitment.application.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.hiring.application.domain.ApplicationType; +import page.clab.api.domain.hiring.recruitment.domain.Recruitment; + +import java.util.List; + +@Getter +@Builder +public class RecruitmentEndDateResponseDto { + + private Long id; + private ApplicationType applicationType; + + public static List toDto(List recruitments) { + return recruitments.stream() + .map(RecruitmentEndDateResponseDto::toDto) + .toList(); + } + + public static RecruitmentEndDateResponseDto toDto(Recruitment recruitment) { + return RecruitmentEndDateResponseDto.builder() + .id(recruitment.getId()) + .applicationType(recruitment.getApplicationType()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecentRecruitmentsUseCase.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecentRecruitmentsUseCase.java index a9ea5cfcc..a55ed8275 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecentRecruitmentsUseCase.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecentRecruitmentsUseCase.java @@ -1,9 +1,13 @@ package page.clab.api.domain.hiring.recruitment.application.port.in; +import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentEndDateResponseDto; import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentResponseDto; import java.util.List; public interface RetrieveRecentRecruitmentsUseCase { + List retrieveRecentRecruitments(); + + List retrieveRecruitmentsByEndDate(); } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java index 35e1e258c..67627efac 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java @@ -2,6 +2,7 @@ import page.clab.api.domain.hiring.recruitment.domain.Recruitment; +import java.time.LocalDateTime; import java.util.List; public interface RetrieveRecruitmentPort { @@ -12,5 +13,5 @@ public interface RetrieveRecruitmentPort { List findTop5ByOrderByCreatedAtDesc(); - void existsByIdOrThrow(Long recruitmentId); + List findByEndDateBetween(LocalDateTime weekAgo, LocalDateTime now); } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java index 9126fc5a1..038f74be8 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java @@ -3,10 +3,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentEndDateResponseDto; import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentResponseDto; import page.clab.api.domain.hiring.recruitment.application.port.in.RetrieveRecentRecruitmentsUseCase; import page.clab.api.domain.hiring.recruitment.application.port.out.RetrieveRecruitmentPort; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.List; @Service @@ -22,4 +25,16 @@ public List retrieveRecentRecruitments() { .map(RecruitmentResponseDto::toDto) .toList(); } + + @Transactional(readOnly = true) + @Override + public List retrieveRecruitmentsByEndDate() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime weekAgo = now.minusWeeks(1).with(LocalTime.MIN); + LocalDateTime endOfDay = now.with(LocalTime.MAX); + + return retrieveRecruitmentPort.findByEndDateBetween(weekAgo, endOfDay).stream() + .map(RecruitmentEndDateResponseDto::toDto) + .toList(); + } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/domain/Recruitment.java b/src/main/java/page/clab/api/domain/hiring/recruitment/domain/Recruitment.java index e2a36720d..356f5fa17 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/domain/Recruitment.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/domain/Recruitment.java @@ -6,11 +6,13 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.hiring.application.application.exception.RecruitmentEndDateExceededException; import page.clab.api.domain.hiring.application.application.exception.RecruitmentNotActiveException; import page.clab.api.domain.hiring.application.domain.ApplicationType; import page.clab.api.domain.hiring.recruitment.application.dto.request.RecruitmentUpdateRequestDto; import page.clab.api.global.exception.InvalidDateRangeException; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Optional; @@ -56,4 +58,22 @@ public void validateDateRange() { throw new InvalidDateRangeException("시작일은 종료일보다 늦을 수 없습니다."); } } + + /** + * 모집 종료일이 현재 날짜 기준 7일 이내인지 확인하고, 그렇지 않은 경우 예외를 발생시킵니다. + * + * @throws RecruitmentEndDateExceededException 모집 종료일이 현재 날짜 기준 7일을 초과했거나, 아직 모집이 종료되지 않은 경우 발생 + */ + public void validateEndDateWithin7Days() { + LocalDate today = LocalDate.now(); + LocalDate endDate = this.endDate.toLocalDate(); + + if (endDate.isBefore(today.minusDays(7)) || !this.isRecruitmentEnd()) { + throw new RecruitmentEndDateExceededException("지원 기간이 종료된 지 7일이 넘었거나, 아직 종료되지 않은 모집입니다."); + } + } + + public boolean isRecruitmentEnd() { + return LocalDateTime.now().isAfter(endDate) || status.isClosed(); + } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/domain/RecruitmentStatus.java b/src/main/java/page/clab/api/domain/hiring/recruitment/domain/RecruitmentStatus.java index 72cab146e..ab5573fa1 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/domain/RecruitmentStatus.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/domain/RecruitmentStatus.java @@ -17,4 +17,8 @@ public enum RecruitmentStatus { public boolean isRecruiting() { return this.equals(RecruitmentStatus.OPEN); } + + public boolean isClosed() { + return this.equals(RecruitmentStatus.CLOSED); + } } diff --git a/src/main/java/page/clab/api/global/config/SecurityConstants.java b/src/main/java/page/clab/api/global/config/SecurityConstants.java index 36203296b..dc52b07ae 100644 --- a/src/main/java/page/clab/api/global/config/SecurityConstants.java +++ b/src/main/java/page/clab/api/global/config/SecurityConstants.java @@ -17,7 +17,7 @@ public class SecurityConstants { public static final String[] PERMIT_ALL_API_ENDPOINTS_GET = { "/api/v1/applications/{studentId}", - "/api/v1/recruitments", + "/api/v1/recruitments", "/api/v1/recruitments/recent-week", "/api/v1/news", "/api/v1/news/**", "/api/v1/blogs", "/api/v1/blogs/**", "/api/v1/positions", "/api/v1/positions/**", diff --git a/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java b/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java index 54a5976e6..b51718f4e 100644 --- a/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java @@ -39,6 +39,7 @@ import page.clab.api.domain.auth.login.application.exception.MemberLockedException; import page.clab.api.domain.community.accuse.application.exception.AccuseTargetTypeIncorrectException; import page.clab.api.domain.hiring.application.application.exception.NotApprovedApplicationException; +import page.clab.api.domain.hiring.application.application.exception.RecruitmentEndDateExceededException; import page.clab.api.domain.hiring.application.application.exception.RecruitmentNotActiveException; import page.clab.api.domain.library.book.application.exception.BookAlreadyBorrowedException; import page.clab.api.domain.library.book.application.exception.InvalidBorrowerException; @@ -96,6 +97,7 @@ public class GlobalExceptionHandler { InvalidEmojiException.class, InvalidRoleChangeException.class, RecruitmentNotActiveException.class, + RecruitmentEndDateExceededException.class, StringIndexOutOfBoundsException.class, MissingServletRequestParameterException.class, MalformedJsonException.class,