diff --git a/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java b/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java index a0a9226..97d1320 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java @@ -25,4 +25,5 @@ public interface ApplicantRepository extends JpaRepository { Optional findByStudentNoAndContactEndsWith(@Param("studentNo") String studentNo, @Param("contactLastDigits") String contactLastDigits); + Optional findByStudentNoAndEmail(String studentNo, String email); } 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 d2d0519..16759dd 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 @@ -39,7 +39,10 @@ public enum ErrorCode { NOT_FOUND_PARTICIPANT(400, "C030", "참가자를 찾을 수 없습니다."), EXECUTIVE_NOT_FOUND(400, "C031", "임원진을 찾을 수 없습니다."), GENERATION_NOT_FOUND(400, "C032", "저장된 기수를 찾을 수 없습니다."), - INVALID_GENERATION_FORMAT(400, "C033", "유효하지 않은 기수 형식입니다. (예: '1기')"); + INVALID_GENERATION_FORMAT(400, "C033", "유효하지 않은 기수 형식입니다. (예: '1기')"), + VERIFICATION_CODE_NOT_VALID(400, "C034", "인증 코드가 유효하지 않습니다."), + SLOT_UNAVAILABLE(400, "C035", "해당 슬롯을 예약할 수 없습니다.") + ; private final int status; private final String code; diff --git a/src/main/java/dmu/dasom/api/domain/google/enums/MailType.java b/src/main/java/dmu/dasom/api/domain/google/enums/MailType.java index 9ed0ed6..3805dc4 100644 --- a/src/main/java/dmu/dasom/api/domain/google/enums/MailType.java +++ b/src/main/java/dmu/dasom/api/domain/google/enums/MailType.java @@ -2,5 +2,6 @@ public enum MailType { DOCUMENT_RESULT, // 서류 합격 - FINAL_RESULT // 최종 합격 + FINAL_RESULT, // 최종 합격 + VERIFICATION // 이메일 인증 } diff --git a/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java b/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java index 0b5e0c1..40d50a2 100644 --- a/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java +++ b/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java @@ -44,9 +44,8 @@ public void sendEmail(String to, String name, MailType mailType) { // HTML 템플릿에 전달할 데이터 설정 Context context = new Context(); - context.setVariable("name", name); // 지원자 이름 전달 - context.setVariable("buttonUrl", buttonUrl); // 버튼 링크 전달 - + context.setVariable("name", name); + context.setVariable("buttonUrl", buttonUrl); // HTML 템플릿 처리 String htmlBody = templateEngine.process(mailTemplate.getTemplateName(), context); @@ -60,11 +59,10 @@ public void sendEmail(String to, String name, MailType mailType) { helper.setText(htmlBody, true); helper.setFrom((from != null && !from.isEmpty()) ? from : "dasomdmu@gmail.com"); - // Content-Type을 명시적으로 설정 message.setContent(htmlBody, "text/html; charset=utf-8"); javaMailSender.send(message); - log.info("Email sent successfull {}", to); + log.info("Email sent successfully {}", to); } catch (MessagingException e) { log.error("Failed to send email to {}: {}", to, e.getMessage()); mailSendStatus = MailSendStatus.FAILURE; @@ -76,4 +74,35 @@ public void sendEmail(String to, String name, MailType mailType) { } emailLogService.logEmailSending(to, mailSendStatus, errorMessage); } + + /* + * 면접 예약 변경을 위한 인증코드 발송 + * - VerificationCodeManager에서 생성된 코드를 이메일로 전송 + * - verify-num-email.html 템플릿을 이용해 코드와 버튼 링크 포함 + */ + public void sendVerificationEmail(String to, String name, String code) throws MessagingException { + String subject = "DASOM 면접 시간 변경을 위한 이메일 인증 코드 안내"; + + // 인증 코드만 템플릿으로 전달 + String emailContent = "인증 코드: " + code + ""; + + Context context = new Context(); + context.setVariable("name", name); + context.setVariable("emailContent", emailContent); + context.setVariable("buttonUrl", "https://dmu-dasom.or.kr"); + context.setVariable("buttonText", "인증 완료"); + + String htmlBody = templateEngine.process("verify-num-email", context); + + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setTo(to); + helper.setSubject(subject); + helper.setText(htmlBody, true); + helper.setFrom((from != null && !from.isEmpty()) ? from : "dasomdmu@gmail.com"); + + javaMailSender.send(message); + } + } diff --git a/src/main/java/dmu/dasom/api/domain/interview/controller/InterviewController.java b/src/main/java/dmu/dasom/api/domain/interview/controller/InterviewController.java new file mode 100644 index 0000000..0612399 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/interview/controller/InterviewController.java @@ -0,0 +1,79 @@ +package dmu.dasom.api.domain.interview.controller; + +import dmu.dasom.api.domain.applicant.entity.Applicant; +import dmu.dasom.api.domain.applicant.repository.ApplicantRepository; +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; +import dmu.dasom.api.domain.google.service.EmailService; + +import dmu.dasom.api.domain.interview.dto.InterviewReservationModifyRequestDto; +import dmu.dasom.api.domain.interview.dto.VerificationCodeRequestDto; +import dmu.dasom.api.domain.interview.service.InterviewService; +import dmu.dasom.api.global.util.VerificationCodeManager; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import dmu.dasom.api.domain.interview.dto.InterviewReservationApplicantResponseDto; + +@Tag(name = "Interview", description = "면접 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/interview") +public class InterviewController { + + private final InterviewService interviewService; + private final ApplicantRepository applicantRepository; + private final VerificationCodeManager verificationCodeManager; + private final EmailService emailService; // 이메일 발송 서비스 (Google 기반) + + /* + * 면접 예약 수정을 위한 인증 코드 발송 + * - 지원자의 학번을 입력받아 해당 지원자를 조회 + * - VerificationCodeManager를 통해 인증 코드 생성 및 Redis 저장 + * - EmailService를 이용해 지원자 이메일로 인증 코드 발송 + */ + @Operation(summary = "면접 예약 수정을 위한 인증 코드 발송", description = "지원자의 학번을 받아 이메일로 인증 코드를 발송합니다.") + @PostMapping("/send-verification") + public ResponseEntity sendVerificationCode(@Valid @RequestBody VerificationCodeRequestDto request) throws MessagingException { + // 학번으로 지원자 조회 (없으면 예외 발생) + Applicant applicant = applicantRepository.findByStudentNo(request.getStudentNo()) + .orElseThrow(() -> new CustomException(ErrorCode.APPLICANT_NOT_FOUND)); + + // 인증 코드 생성 후 Redis에 저장 + String code = verificationCodeManager.generateAndStoreCode(applicant.getStudentNo()); + + // 이메일 발송 (받는 사람 이메일, 이름, 코드 전달) + emailService.sendVerificationEmail(applicant.getEmail(), applicant.getName(), code); + + return ResponseEntity.ok().build(); + } + + /* + * 면접 예약 수정 + * - 사용자가 받은 인증 코드를 검증한 후 + * - InterviewService를 통해 예약 날짜/시간 수정 처리 + */ + @Operation(summary = "면접 예약 수정", description = "이메일로 발송된 인증 코드를 통해 인증 후, 면접 날짜 및 시간을 수정합니다.") + @PutMapping("/reservation/modify") + public ResponseEntity modifyInterviewReservation(@Valid @RequestBody InterviewReservationModifyRequestDto request) { + interviewService.modifyInterviewReservation(request); + return ResponseEntity.ok().build(); + } + + /* + * 모든 면접 지원자 조회 + * - InterviewService를 통해 모든 지원자 + 예약 정보 반환 + */ + @Operation(summary = "모든 면접 지원자 목록 조회", description = "모든 면접 지원자의 상세 정보와 예약 정보를 조회합니다.") + @GetMapping("/applicants") + public ResponseEntity> getAllInterviewApplicants() { + List applicants = interviewService.getAllInterviewApplicants(); + return ResponseEntity.ok(applicants); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/interview/dto/InterviewReservationModifyRequestDto.java b/src/main/java/dmu/dasom/api/domain/interview/dto/InterviewReservationModifyRequestDto.java new file mode 100644 index 0000000..5658fcb --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/interview/dto/InterviewReservationModifyRequestDto.java @@ -0,0 +1,37 @@ +package dmu.dasom.api.domain.interview.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(name = "InterviewReservationModifyRequestDto", description = "면접 예약 수정 요청 DTO") +public class InterviewReservationModifyRequestDto { + + @NotNull(message = "학번은 필수 값입니다.") + @Pattern(regexp = "^[0-9]{8}$", message = "학번은 8자리 숫자로 구성되어야 합니다.") + @Size(min = 8, max = 8) + @Schema(description = "지원자 학번", example = "20250001") + private String studentNo; + + @NotNull(message = "이메일은 필수 값입니다.") + @Email(message = "유효한 이메일 주소를 입력해주세요.") + @Size(max = 64) + @Schema(description = "지원자 이메일", example = "test@example.com") + private String email; + + @NotNull(message = "새로운 슬롯 ID는 필수 값입니다.") + @Schema(description = "새롭게 예약할 면접 슬롯의 ID", example = "2") + private Long newSlotId; + + @NotNull(message = "인증 코드는 필수 값입니다.") + @Schema(description = "이메일로 발송된 6자리 인증 코드", example = "123456") + private String verificationCode; +} diff --git a/src/main/java/dmu/dasom/api/domain/interview/dto/VerificationCodeRequestDto.java b/src/main/java/dmu/dasom/api/domain/interview/dto/VerificationCodeRequestDto.java new file mode 100644 index 0000000..a43d463 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/interview/dto/VerificationCodeRequestDto.java @@ -0,0 +1,17 @@ +package dmu.dasom.api.domain.interview.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +public class VerificationCodeRequestDto { + + @NotNull(message = "학번은 필수 값입니다.") + @Pattern(regexp = "^[0-9]{8}$", message = "학번은 8자리 숫자로 구성되어야 합니다.") + @Size(min = 8, max = 8) + @Schema(description = "지원자 학번", example = "20250001") + private String studentNo; +} diff --git a/src/main/java/dmu/dasom/api/domain/interview/entity/InterviewReservation.java b/src/main/java/dmu/dasom/api/domain/interview/entity/InterviewReservation.java index 682c47f..33ceb73 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/entity/InterviewReservation.java +++ b/src/main/java/dmu/dasom/api/domain/interview/entity/InterviewReservation.java @@ -36,4 +36,8 @@ public class InterviewReservation { @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; // 생성 + + public void setSlot(InterviewSlot slot) { + this.slot = slot; + } } diff --git a/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewReservationRepository.java b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewReservationRepository.java index 4b7e402..a221c15 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewReservationRepository.java +++ b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewReservationRepository.java @@ -1,10 +1,15 @@ package dmu.dasom.api.domain.interview.repository; import dmu.dasom.api.domain.interview.entity.InterviewReservation; +import dmu.dasom.api.domain.applicant.entity.Applicant; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface InterviewReservationRepository extends JpaRepository { boolean existsByReservationCode(String reservationCode); + Optional findByReservationCode(String reservationCode); + Optional findByApplicant(Applicant applicant); } diff --git a/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java index 67d005c..b13c265 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java +++ b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java @@ -4,6 +4,7 @@ import dmu.dasom.api.domain.interview.enums.InterviewStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; // ★ import 추가 import org.springframework.stereotype.Repository; import java.util.Collection; @@ -12,13 +13,12 @@ @Repository public interface InterviewSlotRepository extends JpaRepository { // 현재 인원이 최대 인원보다 작은 슬롯 조회 - // 현재 예약된 인원이 최대 지원자 수보다 적은 슬롯 조회 @Query("SELECT s FROM InterviewSlot s WHERE s.currentCandidates < s.maxCandidates") Collection findAllByCurrentCandidatesLessThanMaxCandidates(); // 상태에 따른 슬롯 조회 @Query("SELECT s FROM InterviewSlot s WHERE s.interviewStatus = :status AND s.currentCandidates < s.maxCandidates") - List findAllByStatusAndCurrentCandidatesLessThanMaxCandidates(InterviewStatus interviewStatus); + List findAllByStatusAndCurrentCandidatesLessThanMaxCandidates(@Param("status") InterviewStatus interviewStatus); // 슬롯이 하나라도 존재하는지 확인 @Query("SELECT COUNT(s) > 0 FROM InterviewSlot s") diff --git a/src/main/java/dmu/dasom/api/domain/interview/service/InterviewService.java b/src/main/java/dmu/dasom/api/domain/interview/service/InterviewService.java index f58f8ce..e332477 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/service/InterviewService.java +++ b/src/main/java/dmu/dasom/api/domain/interview/service/InterviewService.java @@ -2,6 +2,7 @@ import dmu.dasom.api.domain.interview.dto.InterviewReservationApplicantResponseDto; import dmu.dasom.api.domain.interview.dto.InterviewReservationRequestDto; +import dmu.dasom.api.domain.interview.dto.InterviewReservationModifyRequestDto; import dmu.dasom.api.domain.interview.dto.InterviewSlotResponseDto; import java.time.LocalDate; @@ -27,4 +28,7 @@ public interface InterviewService { List getAllInterviewApplicants(); + // 면접 예약 수정 + void modifyInterviewReservation(InterviewReservationModifyRequestDto request); + } diff --git a/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java b/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java index 6a94895..67d40cd 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java @@ -6,6 +6,7 @@ import dmu.dasom.api.domain.common.exception.ErrorCode; import dmu.dasom.api.domain.interview.dto.InterviewReservationApplicantResponseDto; import dmu.dasom.api.domain.interview.dto.InterviewReservationRequestDto; +import dmu.dasom.api.domain.interview.dto.InterviewReservationModifyRequestDto; import dmu.dasom.api.domain.interview.dto.InterviewSlotResponseDto; import dmu.dasom.api.domain.interview.entity.InterviewReservation; import dmu.dasom.api.domain.interview.entity.InterviewSlot; @@ -13,6 +14,7 @@ import dmu.dasom.api.domain.interview.repository.InterviewReservationRepository; import dmu.dasom.api.domain.interview.repository.InterviewSlotRepository; import dmu.dasom.api.domain.recruit.service.RecruitServiceImpl; +import dmu.dasom.api.global.util.VerificationCodeManager; import jakarta.persistence.EntityListeners; import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -35,6 +37,7 @@ public class InterviewServiceImpl implements InterviewService{ private final InterviewReservationRepository interviewReservationRepository; private final ApplicantRepository applicantRepository; private final RecruitServiceImpl recruitService; + private final VerificationCodeManager verificationCodeManager; @Override @Transactional @@ -172,4 +175,47 @@ public List getAllInterviewApplicants( .collect(Collectors.toList()); } + @Override + @Transactional + public void modifyInterviewReservation(InterviewReservationModifyRequestDto request) { + // 0. 인증 코드 검증 + verificationCodeManager.verifyCode(request.getStudentNo(), request.getVerificationCode()); + + // 1. 지원자 학번과 이메일로 지원자 조회 및 검증 + Applicant applicant = applicantRepository.findByStudentNoAndEmail(request.getStudentNo(), request.getEmail()) + .orElseThrow(() -> new CustomException(ErrorCode.APPLICANT_NOT_FOUND)); + + // 2. 해당 지원자의 기존 면접 예약 조회 + InterviewReservation existingReservation = interviewReservationRepository.findByApplicant(applicant) + .orElseThrow(() -> new CustomException(ErrorCode.RESERVATION_NOT_FOUND)); + + // 3. 새로운 면접 슬롯 조회 및 검증 + InterviewSlot newSlot = interviewSlotRepository.findById(request.getNewSlotId()) + .orElseThrow(() -> new CustomException(ErrorCode.SLOT_NOT_FOUND)); + + // 4. 새로운 슬롯이 현재 예약된 슬롯과 동일한지 확인 (불필요한 업데이트 방지) + if (existingReservation.getSlot().getId().equals(newSlot.getId())) { + return; // 동일한 슬롯으로 변경 요청 시 아무것도 하지 않음 + } + + // 5. 새로운 슬롯의 가용성 확인 + if (newSlot.getCurrentCandidates() >= newSlot.getMaxCandidates() || newSlot.getInterviewStatus() != InterviewStatus.ACTIVE) { + throw new CustomException(ErrorCode.SLOT_UNAVAILABLE); + } + + // 6. 기존 슬롯의 예약 인원 감소 + InterviewSlot oldSlot = existingReservation.getSlot(); + oldSlot.decrementCurrentCandidates(); + interviewSlotRepository.save(oldSlot); // 변경된 oldSlot 저장 + + // 7. 예약 정보 업데이트 (새로운 슬롯으로 변경) + existingReservation.setSlot(newSlot); + + // 8. 새로운 슬롯의 예약 인원 증가 + newSlot.incrementCurrentCandidates(); + interviewSlotRepository.save(newSlot); // 변경된 newSlot 저장 + + // 9. 업데이트된 예약 정보 저장 + interviewReservationRepository.save(existingReservation); + } } 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 f08f00c..008d349 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 @@ -21,8 +21,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.time.LocalDate; -import java.time.LocalTime; import java.util.List; @RestController @@ -38,25 +36,21 @@ 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\": \"이미 등록된 학번입니다.\" }"), - @ExampleObject( - name = "모집 기간 아님", - value = "{ \"code\": \"C029\", \"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\": \"이미 등록된 학번입니다.\" }"), + @ExampleObject(name = "모집 기간 아님", value = "{ \"code\": \"C029\", \"message\": \"모집 기간이 아닙니다.\" }"), + @ExampleObject(name = "잘못된 요청 값", value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }") + })) }) @PostMapping("/apply") public ResponseEntity apply(@Valid @RequestBody final ApplicantCreateRequestDto request) { applicantService.apply(request); - return ResponseEntity.ok() - .build(); + return ResponseEntity.ok().build(); } // 모집 일정 조회 @@ -67,19 +61,46 @@ 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\": \"C016\", \"message\": \"날짜 형식이 올바르지 않습니다.\" }"), + @ExampleObject(name = "잘못된 시간 형식", value = "{ \"code\": \"C017\", \"message\": \"시간 형식이 올바르지 않습니다.\" }"), + @ExampleObject(name = "잘못된 요청 값", value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }") + })) + }) + @PutMapping("/schedule") + public ResponseEntity modifyRecruitSchedule(@RequestBody dmu.dasom.api.domain.recruit.dto.RecruitScheduleModifyRequestDto request) { + recruitService.modifyRecruitSchedule(request); + return ResponseEntity.ok().build(); + } + + // 모집 일정 초기화 (테스트용) + @Operation(summary = "TEMP: 모집 일정 초기화") + @GetMapping("/init-schedule") + public ResponseEntity initSchedule() { + recruitService.initRecruitSchedule(); + return ResponseEntity.ok("Recruit schedule initialized successfully."); + } + // 합격 결과 확인 @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\": \"요청한 값이 올바르지 않습니다.\" }") - })) + @ApiResponse(responseCode = "200", description = "합격 결과 확인 성공"), + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "지원자 없음", value = "{ \"code\": \"C022\", \"message\": \"지원자를 찾을 수 없습니다.\" }"), + @ExampleObject(name = "잘못된 요청 값", value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }") + })) }) @GetMapping("/result") public ResponseEntity checkResult(@ModelAttribute final ResultCheckRequestDto request) { @@ -90,7 +111,17 @@ public ResponseEntity checkResult(@ModelAttribute final @Operation(summary = "면접 예약", description = "지원자가 특정 면접 슬롯을 예약합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "면접 예약 성공"), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터") + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "지원자 없음", value = "{ \"code\": \"C022\", \"message\": \"지원자를 찾을 수 없습니다.\" }"), + @ExampleObject(name = "슬롯 없음", value = "{ \"code\": \"C021\", \"message\": \"슬롯을 찾을 수 없습니다.\" }"), + @ExampleObject(name = "이미 예약됨", value = "{ \"code\": \"C023\", \"message\": \"이미 예약된 지원자입니다.\" }"), + @ExampleObject(name = "슬롯 가득 참", value = "{ \"code\": \"C025\", \"message\": \"해당 슬롯이 가득 찼습니다.\" }"), + @ExampleObject(name = "슬롯 불가", value = "{ \"code\": \"C33\", \"message\": \"해당 슬롯을 예약할 수 없습니다.\" }") + })) }) @PostMapping("/interview/reserve") public ResponseEntity reserveInterviewSlot(@Valid @RequestBody InterviewReservationRequestDto request) { @@ -115,5 +146,4 @@ public ResponseEntity> getAllInterviewSlots() { List allSlots = interviewService.getAllInterviewSlots(); return ResponseEntity.ok(allSlots); } - } 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 cb9a150..1ee30d7 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 @@ -13,7 +13,9 @@ public interface RecruitService { List getRecruitSchedule(); - void modifyRecruitSchedule(final RecruitScheduleModifyRequestDto requestDto); + void modifyRecruitSchedule(RecruitScheduleModifyRequestDto request); + + void initRecruitSchedule(); void modifyGeneration(String newGeneration); 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 cfe4241..4667aa4 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 @@ -88,6 +88,30 @@ public LocalDateTime getResultAnnouncementSchedule(ResultCheckType type) { return parseDateTimeFormat(recruit.getValue()); } + /* + * 모집 일정 초기화 + * - DB에 Recruit 데이터가 존재하지 않을 경우 기본 값으로 초기화 + * - 각 ConfigKey에 대해 Recruit 엔티티를 생성하여 저장 + * - 기본 값은 "2025-01-01T00:00:00"으로 설정됨 + */ + @Override + @Transactional + public void initRecruitSchedule() { + // 이미 데이터가 존재하면 초기화하지 않음 + if (recruitRepository.count() > 0) { + return; + } + + // 모든 ConfigKey를 순회하며 기본 Recruit 데이터 생성 + for (ConfigKey key : ConfigKey.values()) { + Recruit recruit = Recruit.builder() + .key(key) + .value("2025-01-01T00:00:00") // 초기 기본 값 + .build(); + recruitRepository.save(recruit); + } + } + // DB에 저장된 모든 Recruit 객체를 찾아 반환 private List findAll() { return recruitRepository.findAll(); @@ -96,8 +120,7 @@ private List findAll() { // DB에서 key에 해당하는 Recruit 객체를 찾아 반환 private Recruit findByKey(final ConfigKey key) { return recruitRepository.findByKey(key) - .orElseThrow(() -> new CustomException(ErrorCode.ARGUMENT_NOT_VALID)); - + .orElseThrow(() -> new CustomException(ErrorCode.ARGUMENT_NOT_VALID)); } // 시간 형식 변환 및 검증 @@ -117,5 +140,4 @@ private LocalDateTime parseDateTimeFormat(String value) { throw new CustomException(ErrorCode.INVALID_DATETIME_FORMAT); } } - } diff --git a/src/main/java/dmu/dasom/api/global/util/VerificationCodeManager.java b/src/main/java/dmu/dasom/api/global/util/VerificationCodeManager.java new file mode 100644 index 0000000..333da44 --- /dev/null +++ b/src/main/java/dmu/dasom/api/global/util/VerificationCodeManager.java @@ -0,0 +1,47 @@ +package dmu.dasom.api.global.util; + +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class VerificationCodeManager { + + private static final long EXPIRATION_TIME_SECONDS = 180; // 인증 코드 유효 시간 (3분) + private final StringRedisTemplate redisTemplate; + + // RedisTemplate 주입 + public VerificationCodeManager(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * 인증 코드 생성 및 Redis에 저장 + * @param key - 사용자 식별값 (예: 이메일 주소) + * @return 생성된 6자리 인증 코드 + */ + public String generateAndStoreCode(String key) { + String code = String.valueOf((int)(Math.random() * 900000) + 100000); // 6자리 난수 생성 + // Redis에 key=코드 저장, TTL=3분 + redisTemplate.opsForValue().set(key, code, EXPIRATION_TIME_SECONDS, TimeUnit.SECONDS); + return code; + } + + /** + * Redis에서 인증 코드 검증 + * @param key - 사용자 식별값 (예: 이메일 주소) + * @param code - 사용자가 입력한 인증 코드 + * @throws CustomException - 코드가 없거나 일치하지 않을 때 발생 + */ + public void verifyCode(String key, String code) { + String storedCode = redisTemplate.opsForValue().get(key); // Redis에서 코드 조회 + if (storedCode == null || !storedCode.equals(code)) { + throw new CustomException(ErrorCode.VERIFICATION_CODE_NOT_VALID); + } + // 검증 성공 시 코드 삭제 (재사용 방지) + redisTemplate.delete(key); + } +} diff --git a/src/main/resources/templates/verify-num-email.html b/src/main/resources/templates/verify-num-email.html new file mode 100644 index 0000000..88a70d9 --- /dev/null +++ b/src/main/resources/templates/verify-num-email.html @@ -0,0 +1,48 @@ + + + + + + + + 이메일 인증 안내 + + +
+ +
+
+ 로고 +
+
+ +
DASOM
+ +

+ +

+ 컴퓨터공학부 전공동아리 다솜입니다. +

+ +

+ 요청하신 인증 코드는 다음과 같습니다.
+
+ 해당 코드를 3분 내에 입력하여 인증을 완료해 주세요. +

+ +
+ + + +
+ +

+ 또한, 문의 사항은 본 메일에 회신 또는 아래 번호로 편하게 연락 부탁드립니다.
+ 010-6361-3481 +

+
+ +