diff --git a/build.gradle b/build.gradle index 02e066023..b7b7b90f1 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,9 @@ dependencies { testAnnotationProcessor 'org.projectlombok:lombok' testImplementation 'org.awaitility:awaitility:4.2.0' + // Mail + implementation 'org.springframework.boot:spring-boot-starter-mail' + // Etc implementation platform('software.amazon.awssdk:bom:2.41.4') implementation 'software.amazon.awssdk:s3' diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityCreateRequest.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityCreateRequest.java index 14df833e5..e2847126c 100644 --- a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityCreateRequest.java +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityCreateRequest.java @@ -1,12 +1,18 @@ package com.example.solidconnection.admin.university.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public record AdminHomeUniversityCreateRequest( @NotBlank(message = "협정 대학명은 필수입니다") @Size(max = 100, message = "협정 대학명은 100자 이하여야 합니다") - String name + String name, + @Pattern( + regexp = "^[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)+$", + message = "올바른 이메일 도메인 형식이 아닙니다 (예: inha.edu, inu.ac.kr)" + ) + String emailDomain ) { } diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityResponse.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityResponse.java index 719185202..0ebf8eb31 100644 --- a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityResponse.java +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityResponse.java @@ -4,13 +4,15 @@ public record AdminHomeUniversityResponse( long id, - String name + String name, + String emailDomain ) { public static AdminHomeUniversityResponse from(HomeUniversity homeUniversity) { return new AdminHomeUniversityResponse( homeUniversity.getId(), - homeUniversity.getName() + homeUniversity.getName(), + homeUniversity.getEmailDomain() ); } } diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityUpdateRequest.java index e22473099..90d3c856f 100644 --- a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityUpdateRequest.java @@ -1,12 +1,18 @@ package com.example.solidconnection.admin.university.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public record AdminHomeUniversityUpdateRequest( @NotBlank(message = "협정 대학명은 필수입니다") @Size(max = 100, message = "협정 대학명은 100자 이하여야 합니다") - String name + String name, + @Pattern( + regexp = "^[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)+$", + message = "올바른 이메일 도메인 형식이 아닙니다 (예: inha.edu, inu.ac.kr)" + ) + String emailDomain ) { } diff --git a/src/main/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityService.java b/src/main/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityService.java index bcf1a8edf..94cae18da 100644 --- a/src/main/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityService.java +++ b/src/main/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityService.java @@ -1,6 +1,7 @@ package com.example.solidconnection.admin.university.service; import static com.example.solidconnection.common.exception.ErrorCode.HOME_UNIVERSITY_ALREADY_EXISTS; +import static com.example.solidconnection.common.exception.ErrorCode.HOME_UNIVERSITY_EMAIL_DOMAIN_ALREADY_EXISTS; import static com.example.solidconnection.common.exception.ErrorCode.HOME_UNIVERSITY_HAS_REFERENCES; import static com.example.solidconnection.common.exception.ErrorCode.HOME_UNIVERSITY_NOT_FOUND; @@ -48,7 +49,8 @@ public AdminHomeUniversityResponse getHomeUniversity(Long id) { ) public AdminHomeUniversityResponse createHomeUniversity(AdminHomeUniversityCreateRequest request) { validateNameNotExists(request.name()); - HomeUniversity homeUniversity = new HomeUniversity(null, request.name()); + validateEmailDomainNotExists(request.emailDomain()); + HomeUniversity homeUniversity = new HomeUniversity(null, request.name(), request.emailDomain()); return AdminHomeUniversityResponse.from(homeUniversityRepository.save(homeUniversity)); } @@ -59,6 +61,16 @@ private void validateNameNotExists(String name) { }); } + private void validateEmailDomainNotExists(String emailDomain) { + if (emailDomain == null || emailDomain.isBlank()) { + return; + } + homeUniversityRepository.findByEmailDomain(emailDomain) + .ifPresent(existing -> { + throw new CustomException(HOME_UNIVERSITY_EMAIL_DOMAIN_ALREADY_EXISTS); + }); + } + @Transactional @DefaultCacheOut( key = {"univApplyInfoTextSearch", "university:recommend:general"}, @@ -69,7 +81,8 @@ public AdminHomeUniversityResponse updateHomeUniversity(Long id, AdminHomeUniver HomeUniversity homeUniversity = homeUniversityRepository.findById(id) .orElseThrow(() -> new CustomException(HOME_UNIVERSITY_NOT_FOUND)); validateNameNotDuplicated(request.name(), id); - homeUniversity.update(request.name()); + validateEmailDomainNotDuplicated(request.emailDomain(), id); + homeUniversity.update(request.name(), request.emailDomain()); return AdminHomeUniversityResponse.from(homeUniversity); } @@ -82,6 +95,18 @@ private void validateNameNotDuplicated(String name, Long excludeId) { }); } + private void validateEmailDomainNotDuplicated(String emailDomain, Long excludeId) { + if (emailDomain == null || emailDomain.isBlank()) { + return; + } + homeUniversityRepository.findByEmailDomain(emailDomain) + .ifPresent(existing -> { + if (!existing.getId().equals(excludeId)) { + throw new CustomException(HOME_UNIVERSITY_EMAIL_DOMAIN_ALREADY_EXISTS); + } + }); + } + @Transactional @DefaultCacheOut( key = {"univApplyInfoTextSearch", "university:recommend:general"}, diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 8dc4ea70e..903c7b87d 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -47,6 +47,7 @@ public enum ErrorCode { HOST_UNIVERSITY_HAS_REFERENCES(HttpStatus.CONFLICT.value(), "해당 파견 대학을 참조하는 대학 지원 정보가 존재합니다."), HOME_UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "협정 대학교를 찾을 수 없습니다."), HOME_UNIVERSITY_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 협정 대학입니다."), + HOME_UNIVERSITY_EMAIL_DOMAIN_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 사용 중인 이메일 도메인입니다."), HOME_UNIVERSITY_HAS_REFERENCES(HttpStatus.CONFLICT.value(), "해당 협정 대학을 참조하는 데이터가 존재합니다."), COUNTRY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "국가를 찾을 수 없습니다."), COUNTRY_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 국가를 찾을 수 없습니다."), @@ -78,6 +79,14 @@ public enum ErrorCode { SIGN_IN_FAILED(HttpStatus.UNAUTHORIZED.value(), "로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요."), OAUTH_USER_CANNOT_CHANGE_PASSWORD(HttpStatus.BAD_REQUEST.value(), "소셜 로그인 사용자는 비밀번호를 변경할 수 없습니다."), + // school email verification + SCHOOL_EMAIL_ALREADY_VERIFIED(HttpStatus.BAD_REQUEST.value(), "이미 학교 이메일 인증이 완료되었습니다."), + SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 학교 이메일 도메인입니다."), + SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "학교 이메일 인증 요청을 찾을 수 없습니다. 인증 코드 발송을 다시 요청해주세요."), + SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT(HttpStatus.BAD_REQUEST.value(), "인증 코드가 일치하지 않습니다."), + SCHOOL_EMAIL_VERIFICATION_INFO_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR.value(), "학교 이메일 인증 정보 저장에 실패했습니다."), + SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED(HttpStatus.INTERNAL_SERVER_ERROR.value(), "학교 이메일 인증 정보가 손상되었습니다. 인증 코드 발송을 다시 요청해주세요."), + // s3 S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), S3_CLIENT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 클라이언트 에러 발생"), diff --git a/src/main/java/com/example/solidconnection/common/mail/MailService.java b/src/main/java/com/example/solidconnection/common/mail/MailService.java new file mode 100644 index 000000000..97d23247c --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/mail/MailService.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.common.mail; + +import lombok.RequiredArgsConstructor; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MailService { + + private final JavaMailSender javaMailSender; + + public void sendVerificationEmail(String to, String verificationCode) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject("[Solid Connect] 학교 이메일 인증"); + message.setText("인증 코드: " + verificationCode + "\n\n인증 코드는 5분간 유효합니다."); + javaMailSender.send(message); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java index 00c3077e3..1af2b253f 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java @@ -5,12 +5,16 @@ import com.example.solidconnection.siteuser.dto.LocationUpdateRequest; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.dto.PasswordUpdateRequest; +import com.example.solidconnection.siteuser.dto.SchoolEmailConfirmRequest; +import com.example.solidconnection.siteuser.dto.SchoolEmailRequest; import com.example.solidconnection.siteuser.service.MyPageService; +import com.example.solidconnection.siteuser.service.SchoolEmailService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -23,6 +27,7 @@ class MyPageController { private final MyPageService myPageService; + private final SchoolEmailService schoolEmailService; @GetMapping public ResponseEntity getMyPageInfo( @@ -59,4 +64,22 @@ public ResponseEntity updateLocation( myPageService.updateLocation(siteUserId, request); return ResponseEntity.ok().build(); } + + @PostMapping("/school-email") + public ResponseEntity requestSchoolEmailVerification( + @AuthorizedUser long siteUserId, + @RequestBody @Valid SchoolEmailRequest request + ) { + schoolEmailService.requestSchoolEmailVerification(siteUserId, request.schoolEmail()); + return ResponseEntity.ok().build(); + } + + @PostMapping("/school-email/confirm") + public ResponseEntity confirmSchoolEmail( + @AuthorizedUser long siteUserId, + @RequestBody @Valid SchoolEmailConfirmRequest request + ) { + schoolEmailService.confirmSchoolEmail(siteUserId, request.code()); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 2a53348ad..f072e9eb8 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -159,6 +159,10 @@ public void updateUserStatus(UserStatus status) { this.userStatus = status; } + public void verifySchool(Long homeUniversityId) { + this.homeUniversityId = homeUniversityId; + } + public void becomeMentor() { this.role = Role.MENTOR; } diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailConfirmRequest.java b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailConfirmRequest.java new file mode 100644 index 000000000..14d1d4e18 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailConfirmRequest.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.siteuser.dto; + +import jakarta.validation.constraints.NotBlank; + +public record SchoolEmailConfirmRequest( + @NotBlank(message = "인증 코드는 필수입니다") + String code +) { + +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java new file mode 100644 index 000000000..46f1e9bec --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.siteuser.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record SchoolEmailRequest( + @NotBlank(message = "학교 이메일은 필수입니다") + @Email(message = "올바른 이메일 형식이 아닙니다") + String schoolEmail +) { + +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/SchoolVerificationInfo.java b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolVerificationInfo.java new file mode 100644 index 000000000..cc38071f0 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolVerificationInfo.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.siteuser.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SchoolVerificationInfo { + + private String schoolEmail; + private Long homeUniversityId; + private String code; +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java new file mode 100644 index 000000000..34d22233f --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -0,0 +1,112 @@ +package com.example.solidconnection.siteuser.service; + +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_ALREADY_VERIFIED; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_VERIFICATION_INFO_SAVE_FAILED; +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.mail.MailService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.SchoolVerificationInfo; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.domain.HomeUniversity; +import com.example.solidconnection.university.repository.HomeUniversityRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class SchoolEmailService { + + private static final long VERIFICATION_CODE_TTL_SECONDS = 300; + private static final String KEY_PREFIX = "school-email:"; + + private final SiteUserRepository siteUserRepository; + private final HomeUniversityRepository homeUniversityRepository; + private final MailService mailService; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Transactional + public void requestSchoolEmailVerification(long siteUserId, String schoolEmail) { + SiteUser siteUser = siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + + if (siteUser.getHomeUniversityId() != null) { + throw new CustomException(SCHOOL_EMAIL_ALREADY_VERIFIED); + } + + String domain = extractEmailDomain(schoolEmail); + HomeUniversity homeUniversity = homeUniversityRepository.findByEmailDomain(domain) + .orElseThrow(() -> new CustomException(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED)); + + String code = generateVerificationCode(); + saveVerificationInfo(siteUserId, new SchoolVerificationInfo(schoolEmail, homeUniversity.getId(), code)); + + try { + mailService.sendVerificationEmail(schoolEmail, code); + } catch (Exception e) { + redisTemplate.delete(KEY_PREFIX + siteUserId); + throw e; + } + } + + @Transactional + public void confirmSchoolEmail(long siteUserId, String code) { + SiteUser siteUser = siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + + SchoolVerificationInfo info = getVerificationInfo(siteUserId); + + if (!info.getCode().equals(code)) { + throw new CustomException(SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT); + } + + siteUser.verifySchool(info.getHomeUniversityId()); + redisTemplate.delete(KEY_PREFIX + siteUserId); + } + + private void saveVerificationInfo(long siteUserId, SchoolVerificationInfo info) { + try { + redisTemplate.opsForValue().set( + KEY_PREFIX + siteUserId, + objectMapper.writeValueAsString(info), + VERIFICATION_CODE_TTL_SECONDS, + TimeUnit.SECONDS + ); + } catch (JsonProcessingException e) { + throw new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_SAVE_FAILED); + } + } + + private SchoolVerificationInfo getVerificationInfo(long siteUserId) { + String jsonInfo = redisTemplate.opsForValue().get(KEY_PREFIX + siteUserId); + if (jsonInfo == null) { + throw new CustomException(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND); + } + try { + return objectMapper.readValue(jsonInfo, SchoolVerificationInfo.class); + } catch (JsonProcessingException e) { + redisTemplate.delete(KEY_PREFIX + siteUserId); + throw new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED); + } + } + + private String extractEmailDomain(String email) { + return email.substring(email.indexOf('@') + 1).toLowerCase(); + } + + private String generateVerificationCode() { + return String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000)); + } +} diff --git a/src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java b/src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java index 506491c0f..fad719698 100644 --- a/src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java +++ b/src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java @@ -25,7 +25,11 @@ public class HomeUniversity extends BaseEntity { @Column(name = "name", nullable = false, unique = true, length = 100) private String name; - public void update(String name) { + @Column(name = "email_domain", unique = true, length = 100) + private String emailDomain; + + public void update(String name, String emailDomain) { this.name = name; + this.emailDomain = emailDomain; } } diff --git a/src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java index 0cfc0593c..e79a5730e 100644 --- a/src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java @@ -10,4 +10,6 @@ public interface HomeUniversityRepository extends JpaRepository findAllByIdIn(List ids); Optional findByName(String name); + + Optional findByEmailDomain(String emailDomain); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 58eefa5bf..cf409326c 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -49,8 +49,20 @@ VALUES ('test@test.email', 'yonso', 'https://github.com/nayonsoso.png', 'CONSIDERING', 'MENTEE', '$2a$10$psmwlxPfqWnIlq9JrlQJkuXr1XtjRNsyVOgcTWYZub5jFfn0TML76', 'EMAIL'); -- 12341234 -INSERT INTO home_university (id, name) -VALUES (1, '인하대학교'); +INSERT INTO home_university (id, name, email_domain) +VALUES (1, '인하대학교','inha.edu'); + +INSERT INTO home_university (id, name, email_domain) +VALUES (2, '경희대학교','khu.ac.kr'); + +INSERT INTO home_university (id, name, email_domain) +VALUES (3, '중앙대학교','cau.ac.kr'); + +INSERT INTO home_university (id, name, email_domain) +VALUES (4, '성신여자대학교','sungshin.ac.kr'); + +INSERT INTO home_university (id, name, email_domain) +VALUES (5, '인천대학교','inu.ac.kr'); INSERT INTO host_university(id, country_code, region_code, english_name, format_name, korean_name, accommodation_url, english_course_url, homepage_url, diff --git a/src/main/resources/db/migration/V50__add_email_domain_to_home_university.sql b/src/main/resources/db/migration/V50__add_email_domain_to_home_university.sql new file mode 100644 index 000000000..dde41adc0 --- /dev/null +++ b/src/main/resources/db/migration/V50__add_email_domain_to_home_university.sql @@ -0,0 +1,2 @@ +ALTER TABLE home_university + ADD COLUMN email_domain VARCHAR(100) NULL UNIQUE; diff --git a/src/test/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityServiceTest.java b/src/test/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityServiceTest.java index 425475b01..b4acf9912 100644 --- a/src/test/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityServiceTest.java +++ b/src/test/java/com/example/solidconnection/admin/university/service/AdminHomeUniversityServiceTest.java @@ -103,7 +103,7 @@ class 협정대학_생성 { @Test void 유효한_요청으로_협정대학을_생성하면_성공한다() { // given - AdminHomeUniversityCreateRequest request = new AdminHomeUniversityCreateRequest("인하대학교"); + AdminHomeUniversityCreateRequest request = new AdminHomeUniversityCreateRequest("인하대학교", null); // when AdminHomeUniversityResponse response = adminHomeUniversityService.createHomeUniversity(request); @@ -120,7 +120,7 @@ class 협정대학_생성 { void 이미_존재하는_이름으로_생성하면_예외가_발생한다() { // given homeUniversityFixture.인하대학교(); - AdminHomeUniversityCreateRequest request = new AdminHomeUniversityCreateRequest("인하대학교"); + AdminHomeUniversityCreateRequest request = new AdminHomeUniversityCreateRequest("인하대학교", null); // when & then assertThatCode(() -> adminHomeUniversityService.createHomeUniversity(request)) @@ -136,7 +136,7 @@ class 협정대학_수정 { void 유효한_요청으로_협정대학을_수정하면_성공한다() { // given HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); - AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("연세대학교"); + AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("연세대학교", null); // when AdminHomeUniversityResponse response = adminHomeUniversityService.updateHomeUniversity(homeUniversity.getId(), request); @@ -152,7 +152,7 @@ class 협정대학_수정 { @Test void 존재하지_않는_협정대학을_수정하면_예외가_발생한다() { // given - AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("연세대학교"); + AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("연세대학교", null); // when & then assertThatCode(() -> adminHomeUniversityService.updateHomeUniversity(999L, request)) @@ -164,8 +164,8 @@ class 협정대학_수정 { void 다른_협정대학의_이름으로_수정하면_예외가_발생한다() { // given homeUniversityFixture.인하대학교(); - HomeUniversity other = homeUniversityRepository.save(new HomeUniversity(null, "연세대학교")); - AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("인하대학교"); + HomeUniversity other = homeUniversityRepository.save(new HomeUniversity(null, "연세대학교", null)); + AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("인하대학교", null); // when & then assertThatCode(() -> adminHomeUniversityService.updateHomeUniversity(other.getId(), request)) @@ -177,7 +177,7 @@ class 협정대학_수정 { void 같은_이름으로_수정하면_성공한다() { // given HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); - AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("인하대학교"); + AdminHomeUniversityUpdateRequest request = new AdminHomeUniversityUpdateRequest("인하대학교", null); // when AdminHomeUniversityResponse response = adminHomeUniversityService.updateHomeUniversity(homeUniversity.getId(), request); diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java new file mode 100644 index 000000000..2cf4de360 --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java @@ -0,0 +1,137 @@ +package com.example.solidconnection.siteuser.service; + +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_ALREADY_VERIFIED; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.mail.MailService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.university.domain.HomeUniversity; +import com.example.solidconnection.university.fixture.HomeUniversityFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@TestContainerSpringBootTest +@DisplayName("학교 이메일 인증 서비스 테스트") +class SchoolEmailServiceTest { + + @Autowired + private SchoolEmailService schoolEmailService; + + @MockitoBean + private MailService mailService; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private HomeUniversityFixture homeUniversityFixture; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Nested + @DisplayName("학교 이메일 인증 요청") + class 학교_이메일_인증_요청 { + + @Test + void 인증_코드가_발급되고_이메일이_발송된다() { + // given + homeUniversityFixture.인하대학교(); + SiteUser siteUser = siteUserFixture.사용자(); + + // when & then + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); + then(mailService).should().sendVerificationEmail(eq("test@inha.edu"), any()); + } + + @Test + void 이미_학교_인증된_사용자는_예외가_발생한다() { + // given + HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); + SiteUser siteUser = siteUserFixture.국내_대학_정보_소지_사용자(homeUniversity.getId()); + + // when & then + assertThatThrownBy(() -> + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu")) + .isInstanceOf(CustomException.class) + .hasMessage(SCHOOL_EMAIL_ALREADY_VERIFIED.getMessage()); + } + + @Test + void 지원하지_않는_이메일_도메인은_예외가_발생한다() { + // given + SiteUser siteUser = siteUserFixture.사용자(); + + // when & then + assertThatThrownBy(() -> + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@unknown.ac.kr")) + .isInstanceOf(CustomException.class) + .hasMessage(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED.getMessage()); + } + } + + @Nested + @DisplayName("학교 이메일 인증 확인") + class 학교_이메일_인증_확인 { + + @Test + void 인증_코드가_일치하면_homeUniversityId가_설정되고_인증이_완료된다() { + // given + HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); + SiteUser siteUser = siteUserFixture.사용자(); + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); + + ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); + then(mailService).should().sendVerificationEmail(any(), codeCaptor.capture()); + String code = codeCaptor.getValue(); + + // When + schoolEmailService.confirmSchoolEmail(siteUser.getId(), code); + + // Then + SiteUser updated = siteUserRepository.findById(siteUser.getId()).orElseThrow(); + assertThat(updated.getHomeUniversityId()).isEqualTo(homeUniversity.getId()); + } + + @Test + void 인증_정보가_없으면_예외가_발생한다() { + // given + SiteUser siteUser = siteUserFixture.사용자(); + + // when & then + assertThatThrownBy(() -> + schoolEmailService.confirmSchoolEmail(siteUser.getId(), "123456")) + .isInstanceOf(CustomException.class) + .hasMessage(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND.getMessage()); + } + + @Test + void 인증_코드가_다르면_예외가_발생한다() { + // given + homeUniversityFixture.인하대학교(); + SiteUser siteUser = siteUserFixture.사용자(); + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); + + // when & then + assertThatThrownBy(() -> + schoolEmailService.confirmSchoolEmail(siteUser.getId(), "000000")) + .isInstanceOf(CustomException.class) + .hasMessage(SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT.getMessage()); + } + } +} diff --git a/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java index 38ae070e3..e225004d6 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java +++ b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java @@ -13,12 +13,14 @@ public class HomeUniversityFixture { public HomeUniversity 인하대학교() { return homeUniversityFixtureBuilder.homeUniversity() .name("인하대학교") + .emailDomain("inha.edu") .create(); } public HomeUniversity 인천대학교() { return homeUniversityFixtureBuilder.homeUniversity() .name("인천대학교") + .emailDomain("inu.ac.kr") .create(); } } diff --git a/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java index 092b2a0c2..db1326baa 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java +++ b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java @@ -12,6 +12,7 @@ public class HomeUniversityFixtureBuilder { private final HomeUniversityRepository homeUniversityRepository; private String name; + private String emailDomain; public HomeUniversityFixtureBuilder homeUniversity() { return new HomeUniversityFixtureBuilder(homeUniversityRepository); @@ -22,8 +23,13 @@ public HomeUniversityFixtureBuilder name(String name) { return this; } + public HomeUniversityFixtureBuilder emailDomain(String emailDomain) { + this.emailDomain = emailDomain; + return this; + } + public HomeUniversity create() { return homeUniversityRepository.findByName(name) - .orElseGet(() -> homeUniversityRepository.save(new HomeUniversity(null, name))); + .orElseGet(() -> homeUniversityRepository.save(new HomeUniversity(null, name, emailDomain))); } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 165e12a53..8fc126ba9 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -19,6 +19,11 @@ spring: format_sql: true flyway: enabled: false + mail: + host: localhost + port: 25 + username: test + password: test # cloud cloud: