Skip to content

Commit de017c4

Browse files
NADELYSShodoon
andauthored
refactor: 면접 예약 날짜 변경 기능 추가 (DASOMBE-18)
* DASOMBE-18 지원자 학번,이메일 확인후 면접 예약 및 날자 변경 기능(테스트 못해봄.) * DASOMBE-18 면접 지원자 예약 날짜 변경 테스트 * DASOMBE-18 이메일 인증 * DASOMBE-18 <피드백 반영. 추가 설명 표시. 인증코드 템플릿 반영> * [refactor] DASOMBE-18 <에러 코드 명세 추가> * refactor: 면접 예약 날짜 변경 기능 추가(DASOMBE-18) --------- Co-authored-by: DoHoon Yoon <[email protected]> Co-authored-by: hodoon <[email protected]>
1 parent 0f7e3ce commit de017c4

File tree

17 files changed

+417
-42
lines changed

17 files changed

+417
-42
lines changed

src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ public interface ApplicantRepository extends JpaRepository<Applicant, Long> {
2525
Optional<Applicant> findByStudentNoAndContactEndsWith(@Param("studentNo") String studentNo,
2626
@Param("contactLastDigits") String contactLastDigits);
2727

28+
Optional<Applicant> findByStudentNoAndEmail(String studentNo, String email);
2829
}

src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ public enum ErrorCode {
3939
NOT_FOUND_PARTICIPANT(400, "C030", "참가자를 찾을 수 없습니다."),
4040
EXECUTIVE_NOT_FOUND(400, "C031", "임원진을 찾을 수 없습니다."),
4141
GENERATION_NOT_FOUND(400, "C032", "저장된 기수를 찾을 수 없습니다."),
42-
INVALID_GENERATION_FORMAT(400, "C033", "유효하지 않은 기수 형식입니다. (예: '1기')");
42+
INVALID_GENERATION_FORMAT(400, "C033", "유효하지 않은 기수 형식입니다. (예: '1기')"),
43+
VERIFICATION_CODE_NOT_VALID(400, "C034", "인증 코드가 유효하지 않습니다."),
44+
SLOT_UNAVAILABLE(400, "C035", "해당 슬롯을 예약할 수 없습니다.")
45+
;
4346

4447
private final int status;
4548
private final String code;

src/main/java/dmu/dasom/api/domain/google/enums/MailType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
public enum MailType {
44
DOCUMENT_RESULT, // 서류 합격
5-
FINAL_RESULT // 최종 합격
5+
FINAL_RESULT, // 최종 합격
6+
VERIFICATION // 이메일 인증
67
}

src/main/java/dmu/dasom/api/domain/google/service/EmailService.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ public void sendEmail(String to, String name, MailType mailType) {
4444

4545
// HTML 템플릿에 전달할 데이터 설정
4646
Context context = new Context();
47-
context.setVariable("name", name); // 지원자 이름 전달
48-
context.setVariable("buttonUrl", buttonUrl); // 버튼 링크 전달
49-
47+
context.setVariable("name", name);
48+
context.setVariable("buttonUrl", buttonUrl);
5049

5150
// HTML 템플릿 처리
5251
String htmlBody = templateEngine.process(mailTemplate.getTemplateName(), context);
@@ -60,11 +59,10 @@ public void sendEmail(String to, String name, MailType mailType) {
6059
helper.setText(htmlBody, true);
6160
helper.setFrom((from != null && !from.isEmpty()) ? from : "[email protected]");
6261

63-
// Content-Type을 명시적으로 설정
6462
message.setContent(htmlBody, "text/html; charset=utf-8");
6563

6664
javaMailSender.send(message);
67-
log.info("Email sent successfull {}", to);
65+
log.info("Email sent successfully {}", to);
6866
} catch (MessagingException e) {
6967
log.error("Failed to send email to {}: {}", to, e.getMessage());
7068
mailSendStatus = MailSendStatus.FAILURE;
@@ -76,4 +74,35 @@ public void sendEmail(String to, String name, MailType mailType) {
7674
}
7775
emailLogService.logEmailSending(to, mailSendStatus, errorMessage);
7876
}
77+
78+
/*
79+
* 면접 예약 변경을 위한 인증코드 발송
80+
* - VerificationCodeManager에서 생성된 코드를 이메일로 전송
81+
* - verify-num-email.html 템플릿을 이용해 코드와 버튼 링크 포함
82+
*/
83+
public void sendVerificationEmail(String to, String name, String code) throws MessagingException {
84+
String subject = "DASOM 면접 시간 변경을 위한 이메일 인증 코드 안내";
85+
86+
// 인증 코드만 템플릿으로 전달
87+
String emailContent = "인증 코드: <strong>" + code + "</strong>";
88+
89+
Context context = new Context();
90+
context.setVariable("name", name);
91+
context.setVariable("emailContent", emailContent);
92+
context.setVariable("buttonUrl", "https://dmu-dasom.or.kr");
93+
context.setVariable("buttonText", "인증 완료");
94+
95+
String htmlBody = templateEngine.process("verify-num-email", context);
96+
97+
MimeMessage message = javaMailSender.createMimeMessage();
98+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
99+
100+
helper.setTo(to);
101+
helper.setSubject(subject);
102+
helper.setText(htmlBody, true);
103+
helper.setFrom((from != null && !from.isEmpty()) ? from : "[email protected]");
104+
105+
javaMailSender.send(message);
106+
}
107+
79108
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package dmu.dasom.api.domain.interview.controller;
2+
3+
import dmu.dasom.api.domain.applicant.entity.Applicant;
4+
import dmu.dasom.api.domain.applicant.repository.ApplicantRepository;
5+
import dmu.dasom.api.domain.common.exception.CustomException;
6+
import dmu.dasom.api.domain.common.exception.ErrorCode;
7+
import dmu.dasom.api.domain.google.service.EmailService;
8+
9+
import dmu.dasom.api.domain.interview.dto.InterviewReservationModifyRequestDto;
10+
import dmu.dasom.api.domain.interview.dto.VerificationCodeRequestDto;
11+
import dmu.dasom.api.domain.interview.service.InterviewService;
12+
import dmu.dasom.api.global.util.VerificationCodeManager;
13+
import io.swagger.v3.oas.annotations.Operation;
14+
import io.swagger.v3.oas.annotations.tags.Tag;
15+
import jakarta.mail.MessagingException;
16+
import jakarta.validation.Valid;
17+
import lombok.RequiredArgsConstructor;
18+
import org.springframework.http.ResponseEntity;
19+
import org.springframework.web.bind.annotation.*;
20+
21+
import java.util.List;
22+
import dmu.dasom.api.domain.interview.dto.InterviewReservationApplicantResponseDto;
23+
24+
@Tag(name = "Interview", description = "면접 관련 API")
25+
@RestController
26+
@RequiredArgsConstructor
27+
@RequestMapping("/api/interview")
28+
public class InterviewController {
29+
30+
private final InterviewService interviewService;
31+
private final ApplicantRepository applicantRepository;
32+
private final VerificationCodeManager verificationCodeManager;
33+
private final EmailService emailService; // 이메일 발송 서비스 (Google 기반)
34+
35+
/*
36+
* 면접 예약 수정을 위한 인증 코드 발송
37+
* - 지원자의 학번을 입력받아 해당 지원자를 조회
38+
* - VerificationCodeManager를 통해 인증 코드 생성 및 Redis 저장
39+
* - EmailService를 이용해 지원자 이메일로 인증 코드 발송
40+
*/
41+
@Operation(summary = "면접 예약 수정을 위한 인증 코드 발송", description = "지원자의 학번을 받아 이메일로 인증 코드를 발송합니다.")
42+
@PostMapping("/send-verification")
43+
public ResponseEntity<Void> sendVerificationCode(@Valid @RequestBody VerificationCodeRequestDto request) throws MessagingException {
44+
// 학번으로 지원자 조회 (없으면 예외 발생)
45+
Applicant applicant = applicantRepository.findByStudentNo(request.getStudentNo())
46+
.orElseThrow(() -> new CustomException(ErrorCode.APPLICANT_NOT_FOUND));
47+
48+
// 인증 코드 생성 후 Redis에 저장
49+
String code = verificationCodeManager.generateAndStoreCode(applicant.getStudentNo());
50+
51+
// 이메일 발송 (받는 사람 이메일, 이름, 코드 전달)
52+
emailService.sendVerificationEmail(applicant.getEmail(), applicant.getName(), code);
53+
54+
return ResponseEntity.ok().build();
55+
}
56+
57+
/*
58+
* 면접 예약 수정
59+
* - 사용자가 받은 인증 코드를 검증한 후
60+
* - InterviewService를 통해 예약 날짜/시간 수정 처리
61+
*/
62+
@Operation(summary = "면접 예약 수정", description = "이메일로 발송된 인증 코드를 통해 인증 후, 면접 날짜 및 시간을 수정합니다.")
63+
@PutMapping("/reservation/modify")
64+
public ResponseEntity<Void> modifyInterviewReservation(@Valid @RequestBody InterviewReservationModifyRequestDto request) {
65+
interviewService.modifyInterviewReservation(request);
66+
return ResponseEntity.ok().build();
67+
}
68+
69+
/*
70+
* 모든 면접 지원자 조회
71+
* - InterviewService를 통해 모든 지원자 + 예약 정보 반환
72+
*/
73+
@Operation(summary = "모든 면접 지원자 목록 조회", description = "모든 면접 지원자의 상세 정보와 예약 정보를 조회합니다.")
74+
@GetMapping("/applicants")
75+
public ResponseEntity<List<InterviewReservationApplicantResponseDto>> getAllInterviewApplicants() {
76+
List<InterviewReservationApplicantResponseDto> applicants = interviewService.getAllInterviewApplicants();
77+
return ResponseEntity.ok(applicants);
78+
}
79+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dmu.dasom.api.domain.interview.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.Email;
5+
import jakarta.validation.constraints.NotNull;
6+
import jakarta.validation.constraints.Pattern;
7+
import jakarta.validation.constraints.Size;
8+
import lombok.*;
9+
10+
@Getter
11+
@Setter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Builder
15+
@Schema(name = "InterviewReservationModifyRequestDto", description = "면접 예약 수정 요청 DTO")
16+
public class InterviewReservationModifyRequestDto {
17+
18+
@NotNull(message = "학번은 필수 값입니다.")
19+
@Pattern(regexp = "^[0-9]{8}$", message = "학번은 8자리 숫자로 구성되어야 합니다.")
20+
@Size(min = 8, max = 8)
21+
@Schema(description = "지원자 학번", example = "20250001")
22+
private String studentNo;
23+
24+
@NotNull(message = "이메일은 필수 값입니다.")
25+
@Email(message = "유효한 이메일 주소를 입력해주세요.")
26+
@Size(max = 64)
27+
@Schema(description = "지원자 이메일", example = "[email protected]")
28+
private String email;
29+
30+
@NotNull(message = "새로운 슬롯 ID는 필수 값입니다.")
31+
@Schema(description = "새롭게 예약할 면접 슬롯의 ID", example = "2")
32+
private Long newSlotId;
33+
34+
@NotNull(message = "인증 코드는 필수 값입니다.")
35+
@Schema(description = "이메일로 발송된 6자리 인증 코드", example = "123456")
36+
private String verificationCode;
37+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dmu.dasom.api.domain.interview.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Pattern;
6+
import jakarta.validation.constraints.Size;
7+
import lombok.Getter;
8+
9+
@Getter
10+
public class VerificationCodeRequestDto {
11+
12+
@NotNull(message = "학번은 필수 값입니다.")
13+
@Pattern(regexp = "^[0-9]{8}$", message = "학번은 8자리 숫자로 구성되어야 합니다.")
14+
@Size(min = 8, max = 8)
15+
@Schema(description = "지원자 학번", example = "20250001")
16+
private String studentNo;
17+
}

src/main/java/dmu/dasom/api/domain/interview/entity/InterviewReservation.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ public class InterviewReservation {
3636
@CreatedDate
3737
@Column(name = "created_at", nullable = false, updatable = false)
3838
private LocalDateTime createdAt; // 생성
39+
40+
public void setSlot(InterviewSlot slot) {
41+
this.slot = slot;
42+
}
3943
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package dmu.dasom.api.domain.interview.repository;
22

33
import dmu.dasom.api.domain.interview.entity.InterviewReservation;
4+
import dmu.dasom.api.domain.applicant.entity.Applicant;
45
import org.springframework.data.jpa.repository.JpaRepository;
56
import org.springframework.stereotype.Repository;
67

8+
import java.util.Optional;
9+
710
@Repository
811
public interface InterviewReservationRepository extends JpaRepository<InterviewReservation, Long> {
912
boolean existsByReservationCode(String reservationCode);
13+
Optional<InterviewReservation> findByReservationCode(String reservationCode);
14+
Optional<InterviewReservation> findByApplicant(Applicant applicant);
1015
}

src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import dmu.dasom.api.domain.interview.enums.InterviewStatus;
55
import org.springframework.data.jpa.repository.JpaRepository;
66
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param; // ★ import 추가
78
import org.springframework.stereotype.Repository;
89

910
import java.util.Collection;
@@ -12,13 +13,12 @@
1213
@Repository
1314
public interface InterviewSlotRepository extends JpaRepository<InterviewSlot, Long> {
1415
// 현재 인원이 최대 인원보다 작은 슬롯 조회
15-
// 현재 예약된 인원이 최대 지원자 수보다 적은 슬롯 조회
1616
@Query("SELECT s FROM InterviewSlot s WHERE s.currentCandidates < s.maxCandidates")
1717
Collection<InterviewSlot> findAllByCurrentCandidatesLessThanMaxCandidates();
1818

1919
// 상태에 따른 슬롯 조회
2020
@Query("SELECT s FROM InterviewSlot s WHERE s.interviewStatus = :status AND s.currentCandidates < s.maxCandidates")
21-
List<InterviewSlot> findAllByStatusAndCurrentCandidatesLessThanMaxCandidates(InterviewStatus interviewStatus);
21+
List<InterviewSlot> findAllByStatusAndCurrentCandidatesLessThanMaxCandidates(@Param("status") InterviewStatus interviewStatus);
2222

2323
// 슬롯이 하나라도 존재하는지 확인
2424
@Query("SELECT COUNT(s) > 0 FROM InterviewSlot s")

0 commit comments

Comments
 (0)