diff --git a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantService.java b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantService.java index 687c350..d65257c 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantService.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantService.java @@ -19,4 +19,6 @@ public interface ApplicantService { void sendEmailsToApplicants(MailType mailType); + ApplicantDetailsResponseDto getApplicantByStudentNo(final String studentNo); + } diff --git a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java index 8bce02c..aeba1e1 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java @@ -110,6 +110,13 @@ public void sendEmailsToApplicants(MailType mailType) { } } + // 학번으로 지원자 조회 + @Override + public ApplicantDetailsResponseDto getApplicantByStudentNo(final String studentNo) { + return findByStudentNo(studentNo) + .map(Applicant::toApplicantDetailsResponse) + .orElseThrow(() -> new CustomException(ErrorCode.ARGUMENT_NOT_VALID)); + } // Repository에서 ID로 지원자 조회 private Applicant findById(final Long id) { diff --git a/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java b/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java index 7857283..17055fe 100644 --- a/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java +++ b/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java @@ -23,7 +23,8 @@ public enum ErrorCode { SEND_EMAIL_FAIL(400, "C014", "이메일 전송에 실패하였습니다."), MAIL_TYPE_NOT_VALID(400, "C015", "메일 타입이 올바르지 않습니다."), INVALID_DATETIME_FORMAT(400, "C016", "날짜 형식이 올바르지 않습니다."), - INVALID_TIME_FORMAT(400, "C017", "시간 형식이 올바르지 않습니다.") + INVALID_TIME_FORMAT(400, "C017", "시간 형식이 올바르지 않습니다."), + INVALID_INQUIRY_PERIOD(400, "C018", "조회 기간이 아닙니다.") ; private final int status; diff --git a/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java b/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java index 82555c8..920c18d 100644 --- a/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java +++ b/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java @@ -3,6 +3,8 @@ import dmu.dasom.api.domain.applicant.dto.ApplicantCreateRequestDto; import dmu.dasom.api.domain.applicant.service.ApplicantService; import dmu.dasom.api.domain.common.exception.ErrorResponse; +import dmu.dasom.api.domain.recruit.dto.ResultCheckRequestDto; +import dmu.dasom.api.domain.recruit.dto.ResultCheckResponseDto; import dmu.dasom.api.domain.recruit.dto.RecruitConfigResponseDto; import dmu.dasom.api.domain.recruit.service.RecruitService; import io.swagger.v3.oas.annotations.Operation; @@ -29,19 +31,22 @@ public class RecruitController { // 지원하기 @Operation(summary = "부원 지원하기") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "지원 성공"), - @ApiResponse(responseCode = "400", description = "실패 케이스", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ErrorResponse.class), - examples = { - @ExampleObject( - name = "학번 중복", - value = "{ \"code\": \"C013\", \"message\": \"이미 등록된 학번입니다.\" }")}))}) + @ApiResponse(responseCode = "200", description = "지원 성공"), + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "학번 중복", + value = "{ \"code\": \"C013\", \"message\": \"이미 등록된 학번입니다.\" }") + })) + }) @PostMapping("/apply") public ResponseEntity apply(@Valid @RequestBody final ApplicantCreateRequestDto request) { applicantService.apply(request); - return ResponseEntity.ok().build(); + return ResponseEntity.ok() + .build(); } // 모집 일정 조회 @@ -52,4 +57,23 @@ public ResponseEntity> getRecruitSchedule() { return ResponseEntity.ok(recruitService.getRecruitSchedule()); } + // 합격 결과 확인 + @Operation(summary = "합격 결과 확인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "합격 결과 확인 성공"), + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "학번 조회 실패 혹은 전화번호 뒷자리 일치하지 않음", + value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }") + })) + }) + @GetMapping("/result") + public ResponseEntity checkResult(@ModelAttribute final ResultCheckRequestDto request) { + return ResponseEntity.ok(recruitService.checkResult(request)); + } + } diff --git a/src/main/java/dmu/dasom/api/domain/recruit/dto/ResultCheckRequestDto.java b/src/main/java/dmu/dasom/api/domain/recruit/dto/ResultCheckRequestDto.java new file mode 100644 index 0000000..8c78a9a --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/recruit/dto/ResultCheckRequestDto.java @@ -0,0 +1,35 @@ +package dmu.dasom.api.domain.recruit.dto; + +import dmu.dasom.api.domain.recruit.enums.ResultCheckType; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; + +@AllArgsConstructor +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ParameterObject +@Schema(name = "ResultCheckRequestDto", description = "지원 결과 확인 요청 DTO") +public class ResultCheckRequestDto { + + @NotNull + @Parameter(description = "지원 결과 조회 타입") + @Schema(example = "DOCUMENT_PASS") + private ResultCheckType type; + + @NotNull + @Parameter(description = "학번") + @Schema(example = "20210000") + private String studentNo; + + @NotNull + @Parameter(description = "전화번호 뒷자리") + @Schema(example = "6789") + private String contactLastDigit; + +} diff --git a/src/main/java/dmu/dasom/api/domain/recruit/dto/ResultCheckResponseDto.java b/src/main/java/dmu/dasom/api/domain/recruit/dto/ResultCheckResponseDto.java new file mode 100644 index 0000000..5cd93b1 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/recruit/dto/ResultCheckResponseDto.java @@ -0,0 +1,33 @@ +package dmu.dasom.api.domain.recruit.dto; + +import dmu.dasom.api.domain.recruit.enums.ResultCheckType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@Schema(name = "ResultCheckResponseDto", description = "지원 결과 확인 응답 DTO") +public class ResultCheckResponseDto { + + @NotNull + @Schema(description = "지원 결과 확인 타입", example = "DOCUMENT_PASS") + ResultCheckType type; + + @NotNull + @Schema(description = "학번", example = "20210000") + @Size(min = 8, max = 8) + String studentNo; + + @NotNull + @Schema(description = "이름", example = "홍길동") + @Size(max = 16) + String name; + + @NotNull + @Schema(description = "결과", example = "true") + Boolean isPassed; + +} diff --git a/src/main/java/dmu/dasom/api/domain/recruit/enums/ResultCheckType.java b/src/main/java/dmu/dasom/api/domain/recruit/enums/ResultCheckType.java new file mode 100644 index 0000000..ccc5a90 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/recruit/enums/ResultCheckType.java @@ -0,0 +1,6 @@ +package dmu.dasom.api.domain.recruit.enums; + +public enum ResultCheckType { + DOCUMENT_PASS, + INTERVIEW_PASS +} diff --git a/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitService.java b/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitService.java index faf7177..6e0da17 100644 --- a/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitService.java +++ b/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitService.java @@ -1,5 +1,7 @@ package dmu.dasom.api.domain.recruit.service; +import dmu.dasom.api.domain.recruit.dto.ResultCheckRequestDto; +import dmu.dasom.api.domain.recruit.dto.ResultCheckResponseDto; import dmu.dasom.api.domain.recruit.dto.RecruitConfigResponseDto; import dmu.dasom.api.domain.recruit.dto.RecruitScheduleModifyRequestDto; @@ -11,4 +13,6 @@ public interface RecruitService { void modifyRecruitSchedule(final RecruitScheduleModifyRequestDto requestDto); + ResultCheckResponseDto checkResult(final ResultCheckRequestDto request); + } diff --git a/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitServiceImpl.java b/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitServiceImpl.java index cff3d4c..08afc0c 100644 --- a/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/recruit/service/RecruitServiceImpl.java @@ -1,11 +1,17 @@ package dmu.dasom.api.domain.recruit.service; +import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto; +import dmu.dasom.api.domain.applicant.enums.ApplicantStatus; +import dmu.dasom.api.domain.applicant.service.ApplicantService; import dmu.dasom.api.domain.common.exception.CustomException; import dmu.dasom.api.domain.common.exception.ErrorCode; +import dmu.dasom.api.domain.recruit.dto.ResultCheckRequestDto; +import dmu.dasom.api.domain.recruit.dto.ResultCheckResponseDto; import dmu.dasom.api.domain.recruit.dto.RecruitConfigResponseDto; import dmu.dasom.api.domain.recruit.dto.RecruitScheduleModifyRequestDto; import dmu.dasom.api.domain.recruit.entity.Recruit; import dmu.dasom.api.domain.recruit.enums.ConfigKey; +import dmu.dasom.api.domain.recruit.enums.ResultCheckType; import dmu.dasom.api.domain.recruit.repository.RecruitRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,6 +29,7 @@ public class RecruitServiceImpl implements RecruitService { private final RecruitRepository recruitRepository; + private final ApplicantService applicantService; // 모집 일정 설정 조회 @Override @@ -49,6 +56,39 @@ public void modifyRecruitSchedule(final RecruitScheduleModifyRequestDto request) config.updateDateTime(dateTime); } + // 합격 결과 확인 + @Override + public ResultCheckResponseDto checkResult(final ResultCheckRequestDto request) { + final Recruit recruit = switch (request.getType()) { + case DOCUMENT_PASS -> findByKey(ConfigKey.DOCUMENT_PASS_ANNOUNCEMENT); + case INTERVIEW_PASS -> findByKey(ConfigKey.INTERVIEW_PASS_ANNOUNCEMENT); + }; + final LocalDateTime parsedTime = parseDateTimeFormat(recruit.getValue()); + + // 설정 된 시간이 현재 시간보다 이전인 경우 예외 발생 + final LocalDateTime now = LocalDateTime.now(); + if (now.isBefore(parsedTime)) + throw new CustomException(ErrorCode.INVALID_INQUIRY_PERIOD); + + final ApplicantDetailsResponseDto applicant = applicantService.getApplicantByStudentNo(request.getStudentNo()); + + // 연락처 뒷자리가 일치하지 않을 경우 예외 발생 + if (!applicant.getContact().split("-")[2].equals(request.getContactLastDigit())) + throw new CustomException(ErrorCode.ARGUMENT_NOT_VALID); + + // 합격 여부 반환 + return ResultCheckResponseDto.builder() + .type(request.getType()) + .studentNo(applicant.getStudentNo()) + .name(applicant.getName()) + .isPassed(request.getType().equals(ResultCheckType.DOCUMENT_PASS) ? + applicant.getStatus() + .equals(ApplicantStatus.DOCUMENT_PASSED) : + applicant.getStatus() + .equals(ApplicantStatus.INTERVIEW_PASSED)) + .build(); + } + // DB에 저장된 모든 Recruit 객체를 찾아 반환 private List findAll() { return recruitRepository.findAll(); diff --git a/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java b/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java index f9b89af..64b99fc 100644 --- a/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java +++ b/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java @@ -1,6 +1,7 @@ package dmu.dasom.api.domain.applicant; import dmu.dasom.api.domain.applicant.dto.ApplicantCreateRequestDto; +import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto; import dmu.dasom.api.domain.applicant.dto.ApplicantResponseDto; import dmu.dasom.api.domain.applicant.entity.Applicant; import dmu.dasom.api.domain.applicant.enums.ApplicantStatus; @@ -176,4 +177,39 @@ void sendEmailsToApplicants_finalResult() throws MessagingException { verify(emailService).sendEmail("passed@example.com", "합격자", mailType); verify(emailService).sendEmail("failed@example.com", "불합격자", mailType); } + + @Test + @DisplayName("학번으로 지원자 조회 - 성공") + void getApplicantByStudentNo_success() { + // given + String studentNo = "20210000"; + Applicant applicant = mock(Applicant.class); + when(applicantRepository.findByStudentNo(studentNo)).thenReturn(Optional.of(applicant)); + when(applicant.toApplicantDetailsResponse()).thenReturn(mock(ApplicantDetailsResponseDto.class)); + + // when + ApplicantDetailsResponseDto applicantByStudentNo = applicantService.getApplicantByStudentNo(studentNo); + + // then + assertNotNull(applicantByStudentNo); + verify(applicantRepository).findByStudentNo(studentNo); + } + + @Test + @DisplayName("학번으로 지원자 조회 - 실패 (결과 없음)") + void getApplicantByStudentNo_fail() { + // given + String studentNo = "20210000"; + when(applicantRepository.findByStudentNo(studentNo)).thenReturn(Optional.empty()); + + // when + CustomException exception = assertThrows(CustomException.class, () -> { + applicantService.getApplicantByStudentNo(studentNo); + }); + + // then + verify(applicantRepository).findByStudentNo(studentNo); + assertEquals(ErrorCode.ARGUMENT_NOT_VALID, exception.getErrorCode()); + } + } \ No newline at end of file diff --git a/src/test/java/dmu/dasom/api/domain/recruit/RecruitServiceTest.java b/src/test/java/dmu/dasom/api/domain/recruit/RecruitServiceTest.java index a7a7326..3160243 100644 --- a/src/test/java/dmu/dasom/api/domain/recruit/RecruitServiceTest.java +++ b/src/test/java/dmu/dasom/api/domain/recruit/RecruitServiceTest.java @@ -1,11 +1,17 @@ package dmu.dasom.api.domain.recruit; +import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto; +import dmu.dasom.api.domain.applicant.enums.ApplicantStatus; +import dmu.dasom.api.domain.applicant.service.ApplicantServiceImpl; import dmu.dasom.api.domain.common.exception.CustomException; import dmu.dasom.api.domain.common.exception.ErrorCode; +import dmu.dasom.api.domain.recruit.dto.ResultCheckRequestDto; +import dmu.dasom.api.domain.recruit.dto.ResultCheckResponseDto; import dmu.dasom.api.domain.recruit.dto.RecruitConfigResponseDto; import dmu.dasom.api.domain.recruit.dto.RecruitScheduleModifyRequestDto; import dmu.dasom.api.domain.recruit.entity.Recruit; import dmu.dasom.api.domain.recruit.enums.ConfigKey; +import dmu.dasom.api.domain.recruit.enums.ResultCheckType; import dmu.dasom.api.domain.recruit.repository.RecruitRepository; import dmu.dasom.api.domain.recruit.service.RecruitServiceImpl; import org.junit.jupiter.api.DisplayName; @@ -16,6 +22,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; @@ -29,6 +36,9 @@ class RecruitServiceTest { @Mock private RecruitRepository recruitRepository; + @Mock + private ApplicantServiceImpl applicantService; + @InjectMocks private RecruitServiceImpl recruitService; @@ -83,4 +93,90 @@ void modifyRecruitSchedule_fail() { // then assertEquals(ErrorCode.INVALID_TIME_FORMAT, exception.getErrorCode()); } + + @Test + @DisplayName("합격 결과 확인 - 성공") + void checkResult_success() { + // given + Recruit recruit = mock(Recruit.class); + when(recruitRepository.findByKey(ConfigKey.DOCUMENT_PASS_ANNOUNCEMENT)).thenReturn(Optional.of(recruit)); + String pastDateTime = LocalDateTime.now().minusHours(1) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + when(recruit.getValue()).thenReturn(pastDateTime); + + ResultCheckRequestDto request = mock(ResultCheckRequestDto.class); + when(request.getType()).thenReturn(ResultCheckType.DOCUMENT_PASS); + when(request.getStudentNo()).thenReturn("20210000"); + when(request.getContactLastDigit()).thenReturn("1234"); + + ApplicantDetailsResponseDto applicant = mock(ApplicantDetailsResponseDto.class); + when(applicant.getContact()).thenReturn("010-5678-1234"); + when(applicant.getStatus()).thenReturn(ApplicantStatus.DOCUMENT_PASSED); + when(applicant.getStudentNo()).thenReturn("20210000"); + when(applicant.getName()).thenReturn("TestName"); + when(applicantService.getApplicantByStudentNo("20210000")).thenReturn(applicant); + + // when + ResultCheckResponseDto result = recruitService.checkResult(request); + + // then + assertNotNull(result); + assertTrue(result.getIsPassed()); + verify(recruitRepository, times(1)).findByKey(ConfigKey.DOCUMENT_PASS_ANNOUNCEMENT); + verify(applicantService, times(1)).getApplicantByStudentNo("20210000"); + } + + @Test + @DisplayName("합격 결과 확인 - 실패 (확인 기간 아님)") + void checkResult_fail_inquiryPeriod() { + // given + Recruit recruit = mock(Recruit.class); + when(recruitRepository.findByKey(ConfigKey.DOCUMENT_PASS_ANNOUNCEMENT)).thenReturn(Optional.of(recruit)); + String futureDateTime = LocalDateTime.now().plusHours(1) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + when(recruit.getValue()).thenReturn(futureDateTime); + + ResultCheckRequestDto request = mock(ResultCheckRequestDto.class); + when(request.getType()).thenReturn(ResultCheckType.DOCUMENT_PASS); + + // when + CustomException exception = assertThrows(CustomException.class, () -> { + recruitService.checkResult(request); + }); + + // then + assertEquals(ErrorCode.INVALID_INQUIRY_PERIOD, exception.getErrorCode()); + verify(recruitRepository, times(1)).findByKey(ConfigKey.DOCUMENT_PASS_ANNOUNCEMENT); + } + + @Test + @DisplayName("합격 결과 확인 - 실패 (연락처 뒷자리 불일치)") + void checkResult_fail_contactMismatch() { + // given + Recruit recruit = mock(Recruit.class); + when(recruitRepository.findByKey(ConfigKey.DOCUMENT_PASS_ANNOUNCEMENT)).thenReturn(Optional.of(recruit)); + String pastDateTime = LocalDateTime.now().minusHours(1) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + when(recruit.getValue()).thenReturn(pastDateTime); + + ResultCheckRequestDto request = mock(ResultCheckRequestDto.class); + when(request.getType()).thenReturn(ResultCheckType.DOCUMENT_PASS); + when(request.getStudentNo()).thenReturn("20210000"); + when(request.getContactLastDigit()).thenReturn("0000"); + + ApplicantDetailsResponseDto applicant = mock(ApplicantDetailsResponseDto.class); + when(applicant.getContact()).thenReturn("010-5678-1234"); + when(applicantService.getApplicantByStudentNo("20210000")).thenReturn(applicant); + + // when + CustomException exception = assertThrows(CustomException.class, () -> { + recruitService.checkResult(request); + }); + + // then + assertEquals(ErrorCode.ARGUMENT_NOT_VALID, exception.getErrorCode()); + verify(recruitRepository, times(1)).findByKey(ConfigKey.DOCUMENT_PASS_ANNOUNCEMENT); + verify(applicantService, times(1)).getApplicantByStudentNo("20210000"); + } + }