From 09e40cf714f3034ab5aa36786f5f4bc520a6c3f5 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 19:19:56 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=ED=95=99=EA=B5=90=EB=B3=84=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20HomeUniversity=20=EC=9E=90=EB=8F=99=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20(#751)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HomeUniversity에 emailDomain 컬럼 추가 (V50 마이그레이션) - HomeUniversityRepository에 findByEmailDomain 메서드 추가 - EmailService 구현 (JavaMailSender 기반 인증 코드 발송) - SchoolEmailService 구현 (인증 코드 발급/확인, Redis TTL 5분) - SiteUser.verifySchool() 도메인 메서드 추가 - MyPageController에 POST /my/school-email, POST /my/school-email/confirm 엔드포인트 추가 - 어드민 HomeUniversity DTO에 emailDomain 필드 반영 Co-Authored-By: Claude Sonnet 4.6 --- build.gradle | 3 + .../dto/AdminHomeUniversityCreateRequest.java | 4 +- .../dto/AdminHomeUniversityResponse.java | 6 +- .../dto/AdminHomeUniversityUpdateRequest.java | 4 +- .../service/AdminHomeUniversityService.java | 4 +- .../common/exception/ErrorCode.java | 6 + .../email/service/EmailService.java | 21 +++ .../siteuser/controller/MyPageController.java | 25 ++++ .../siteuser/domain/SiteUser.java | 4 + .../dto/SchoolEmailConfirmRequest.java | 10 ++ .../siteuser/dto/SchoolEmailRequest.java | 12 ++ .../siteuser/dto/SchoolEmailResponse.java | 7 + .../siteuser/dto/SchoolVerificationInfo.java | 15 ++ .../siteuser/service/SchoolEmailService.java | 113 ++++++++++++++ .../university/domain/HomeUniversity.java | 6 +- .../repository/HomeUniversityRepository.java | 2 + ...0__add_email_domain_to_home_university.sql | 2 + .../AdminHomeUniversityServiceTest.java | 14 +- .../service/SchoolEmailServiceTest.java | 139 ++++++++++++++++++ .../fixture/HomeUniversityFixture.java | 2 + .../fixture/HomeUniversityFixtureBuilder.java | 8 +- src/test/resources/application.yml | 5 + 22 files changed, 397 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/email/service/EmailService.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailConfirmRequest.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailResponse.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/SchoolVerificationInfo.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java create mode 100644 src/main/resources/db/migration/V50__add_email_domain_to_home_university.sql create mode 100644 src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java 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..875ee98f0 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 @@ -6,7 +6,9 @@ public record AdminHomeUniversityCreateRequest( @NotBlank(message = "협정 대학명은 필수입니다") @Size(max = 100, message = "협정 대학명은 100자 이하여야 합니다") - String name + String name, + @Size(max = 100, message = "이메일 도메인은 100자 이하여야 합니다") + 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..194ebf425 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 @@ -6,7 +6,9 @@ public record AdminHomeUniversityUpdateRequest( @NotBlank(message = "협정 대학명은 필수입니다") @Size(max = 100, message = "협정 대학명은 100자 이하여야 합니다") - String name + String name, + @Size(max = 100, message = "이메일 도메인은 100자 이하여야 합니다") + 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..906ad2dab 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 @@ -48,7 +48,7 @@ public AdminHomeUniversityResponse getHomeUniversity(Long id) { ) public AdminHomeUniversityResponse createHomeUniversity(AdminHomeUniversityCreateRequest request) { validateNameNotExists(request.name()); - HomeUniversity homeUniversity = new HomeUniversity(null, request.name()); + HomeUniversity homeUniversity = new HomeUniversity(null, request.name(), request.emailDomain()); return AdminHomeUniversityResponse.from(homeUniversityRepository.save(homeUniversity)); } @@ -69,7 +69,7 @@ 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()); + homeUniversity.update(request.name(), request.emailDomain()); return AdminHomeUniversityResponse.from(homeUniversity); } 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..8edb30cd2 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -78,6 +78,12 @@ 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(), "인증 코드가 일치하지 않습니다."), + // 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/email/service/EmailService.java b/src/main/java/com/example/solidconnection/email/service/EmailService.java new file mode 100644 index 000000000..4d545b310 --- /dev/null +++ b/src/main/java/com/example/solidconnection/email/service/EmailService.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.email.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EmailService { + + 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..5b1bc7b7a 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,18 @@ 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.dto.SchoolEmailResponse; 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.HttpStatus; 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 +29,7 @@ class MyPageController { private final MyPageService myPageService; + private final SchoolEmailService schoolEmailService; @GetMapping public ResponseEntity getMyPageInfo( @@ -59,4 +66,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 + ) { + String schoolEmail = schoolEmailService.requestSchoolEmailVerification(siteUserId, request.schoolEmail()); + return ResponseEntity.status(HttpStatus.ACCEPTED).body(new SchoolEmailResponse(schoolEmail)); + } + + @PostMapping("/school-email/confirm") + public ResponseEntity confirmSchoolEmail( + @AuthorizedUser long siteUserId, + @RequestBody @Valid SchoolEmailConfirmRequest request + ) { + String schoolEmail = schoolEmailService.confirmSchoolEmail(siteUserId, request.code()); + return ResponseEntity.ok(new SchoolEmailResponse(schoolEmail)); + } } 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/SchoolEmailResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailResponse.java new file mode 100644 index 000000000..36fd19de4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailResponse.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.siteuser.dto; + +public record SchoolEmailResponse( + 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..7b0524486 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -0,0 +1,113 @@ +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.USER_NOT_FOUND; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.email.service.EmailService; +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.beans.factory.annotation.Qualifier; +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 EmailService emailService; + @Qualifier("redisTemplate") + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Transactional + public String 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)); + + emailService.sendVerificationEmail(schoolEmail, code); + return schoolEmail; + } + + @Transactional + public String 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); + return info.getSchoolEmail(); + } + + 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 RuntimeException(e); + } + } + + private SchoolVerificationInfo getVerificationInfo(long siteUserId) { + String json = redisTemplate.opsForValue().get(KEY_PREFIX + siteUserId); + if (json == null) { + throw new CustomException(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND); + } + try { + return objectMapper.readValue(json, SchoolVerificationInfo.class); + } catch (JsonProcessingException e) { + redisTemplate.delete(KEY_PREFIX + siteUserId); + throw new CustomException(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND); + } + } + + private String extractEmailDomain(String email) { + int atIndex = email.indexOf('@'); + if (atIndex == -1) { + throw new CustomException(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED); + } + return email.substring(atIndex + 1); + } + + 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/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..4a09b2111 --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java @@ -0,0 +1,139 @@ +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.email.service.EmailService; +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 EmailService emailService; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private HomeUniversityFixture homeUniversityFixture; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Nested + @DisplayName("학교 이메일 인증 요청") + class 학교_이메일_인증_요청 { + + @Test + void 인증_코드가_발급되고_이메일이_발송된다() { + // Given + homeUniversityFixture.인천대학교(); + SiteUser siteUser = siteUserFixture.사용자(); + + // When + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr"); + + // Then + then(emailService).should().sendVerificationEmail(eq("test@inu.ac.kr"), any()); + } + + @Test + void 이미_학교_인증된_사용자는_예외가_발생한다() { + // Given + HomeUniversity homeUniversity = homeUniversityFixture.인천대학교(); + SiteUser siteUser = siteUserFixture.국내_대학_정보_소지_사용자(homeUniversity.getId()); + + // When & Then + assertThatThrownBy(() -> + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr")) + .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@inu.ac.kr"); + + ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); + then(emailService).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@inu.ac.kr"); + + // 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..3ba59fdcc 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.ac.kr") .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: From a9cf908672e1629d3244fcf82d86b08ba868966d Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 21:22:29 +0900 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=20EmailService=EB=A5=BC=20commo?= =?UTF-8?q?n/mail/MailService=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit email 발송은 공통 인프라 관심사이므로 별도 email 패키지 대신 common/mail 패키지로 이동 Co-Authored-By: Claude Sonnet 4.6 --- .../EmailService.java => common/mail/MailService.java} | 4 ++-- .../siteuser/service/SchoolEmailService.java | 6 +++--- .../siteuser/service/SchoolEmailServiceTest.java | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/main/java/com/example/solidconnection/{email/service/EmailService.java => common/mail/MailService.java} (89%) diff --git a/src/main/java/com/example/solidconnection/email/service/EmailService.java b/src/main/java/com/example/solidconnection/common/mail/MailService.java similarity index 89% rename from src/main/java/com/example/solidconnection/email/service/EmailService.java rename to src/main/java/com/example/solidconnection/common/mail/MailService.java index 4d545b310..97d23247c 100644 --- a/src/main/java/com/example/solidconnection/email/service/EmailService.java +++ b/src/main/java/com/example/solidconnection/common/mail/MailService.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.email.service; +package com.example.solidconnection.common.mail; import lombok.RequiredArgsConstructor; import org.springframework.mail.SimpleMailMessage; @@ -7,7 +7,7 @@ @Service @RequiredArgsConstructor -public class EmailService { +public class MailService { private final JavaMailSender javaMailSender; diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java index 7b0524486..a52f086c5 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -7,7 +7,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.email.service.EmailService; +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; @@ -32,7 +32,7 @@ public class SchoolEmailService { private final SiteUserRepository siteUserRepository; private final HomeUniversityRepository homeUniversityRepository; - private final EmailService emailService; + private final MailService mailService; @Qualifier("redisTemplate") private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; @@ -53,7 +53,7 @@ public String requestSchoolEmailVerification(long siteUserId, String schoolEmail String code = generateVerificationCode(); saveVerificationInfo(siteUserId, new SchoolVerificationInfo(schoolEmail, homeUniversity.getId(), code)); - emailService.sendVerificationEmail(schoolEmail, code); + mailService.sendVerificationEmail(schoolEmail, code); return schoolEmail; } diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java index 4a09b2111..ff49db6d6 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java @@ -11,7 +11,7 @@ import static org.mockito.BDDMockito.then; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.email.service.EmailService; +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; @@ -33,7 +33,7 @@ class SchoolEmailServiceTest { private SchoolEmailService schoolEmailService; @MockitoBean - private EmailService emailService; + private MailService mailService; @Autowired private SiteUserFixture siteUserFixture; @@ -58,7 +58,7 @@ class 학교_이메일_인증_요청 { schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr"); // Then - then(emailService).should().sendVerificationEmail(eq("test@inu.ac.kr"), any()); + then(mailService).should().sendVerificationEmail(eq("test@inu.ac.kr"), any()); } @Test @@ -99,7 +99,7 @@ class 학교_이메일_인증_확인 { schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr"); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); - then(emailService).should().sendVerificationEmail(any(), codeCaptor.capture()); + then(mailService).should().sendVerificationEmail(any(), codeCaptor.capture()); String code = codeCaptor.getValue(); // When From 7ea551b0a0a9a2d28517e52312a3776eb5dae42d Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 22:11:40 +0900 Subject: [PATCH 03/11] =?UTF-8?q?refactor:=20=ED=95=99=EA=B5=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20API=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20body=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 클라이언트가 이미 알고 있는 이메일을 응답으로 돌려줄 필요가 없으므로 SchoolEmailResponse 제거 및 반환 타입을 void로 변경 Co-Authored-By: Claude Sonnet 4.6 --- .../siteuser/controller/MyPageController.java | 14 ++++++-------- .../siteuser/dto/SchoolEmailResponse.java | 7 ------- .../siteuser/service/SchoolEmailService.java | 6 ++---- .../siteuser/service/SchoolEmailServiceTest.java | 4 +--- 4 files changed, 9 insertions(+), 22 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailResponse.java 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 5b1bc7b7a..1af2b253f 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java @@ -7,12 +7,10 @@ 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.dto.SchoolEmailResponse; 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.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -68,20 +66,20 @@ public ResponseEntity updateLocation( } @PostMapping("/school-email") - public ResponseEntity requestSchoolEmailVerification( + public ResponseEntity requestSchoolEmailVerification( @AuthorizedUser long siteUserId, @RequestBody @Valid SchoolEmailRequest request ) { - String schoolEmail = schoolEmailService.requestSchoolEmailVerification(siteUserId, request.schoolEmail()); - return ResponseEntity.status(HttpStatus.ACCEPTED).body(new SchoolEmailResponse(schoolEmail)); + schoolEmailService.requestSchoolEmailVerification(siteUserId, request.schoolEmail()); + return ResponseEntity.ok().build(); } @PostMapping("/school-email/confirm") - public ResponseEntity confirmSchoolEmail( + public ResponseEntity confirmSchoolEmail( @AuthorizedUser long siteUserId, @RequestBody @Valid SchoolEmailConfirmRequest request ) { - String schoolEmail = schoolEmailService.confirmSchoolEmail(siteUserId, request.code()); - return ResponseEntity.ok(new SchoolEmailResponse(schoolEmail)); + schoolEmailService.confirmSchoolEmail(siteUserId, request.code()); + return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailResponse.java deleted file mode 100644 index 36fd19de4..000000000 --- a/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.solidconnection.siteuser.dto; - -public record SchoolEmailResponse( - String schoolEmail -) { - -} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java index a52f086c5..072577a40 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -38,7 +38,7 @@ public class SchoolEmailService { private final ObjectMapper objectMapper; @Transactional - public String requestSchoolEmailVerification(long siteUserId, String schoolEmail) { + public void requestSchoolEmailVerification(long siteUserId, String schoolEmail) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); @@ -54,11 +54,10 @@ public String requestSchoolEmailVerification(long siteUserId, String schoolEmail saveVerificationInfo(siteUserId, new SchoolVerificationInfo(schoolEmail, homeUniversity.getId(), code)); mailService.sendVerificationEmail(schoolEmail, code); - return schoolEmail; } @Transactional - public String confirmSchoolEmail(long siteUserId, String code) { + public void confirmSchoolEmail(long siteUserId, String code) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); @@ -70,7 +69,6 @@ public String confirmSchoolEmail(long siteUserId, String code) { siteUser.verifySchool(info.getHomeUniversityId()); redisTemplate.delete(KEY_PREFIX + siteUserId); - return info.getSchoolEmail(); } private void saveVerificationInfo(long siteUserId, SchoolVerificationInfo info) { diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java index ff49db6d6..bcdd6a5a4 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java @@ -54,10 +54,8 @@ class 학교_이메일_인증_요청 { homeUniversityFixture.인천대학교(); SiteUser siteUser = siteUserFixture.사용자(); - // When + // When & Then schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr"); - - // Then then(mailService).should().sendVerificationEmail(eq("test@inu.ac.kr"), any()); } From 0b9ddd1e3f40547bdb0ff2e249c9c9fab60014e5 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 22:28:29 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20Redis=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20CustomException=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RuntimeException 대신 SCHOOL_EMAIL_VERIFICATION_INFO_SAVE_FAILED ErrorCode를 사용하여 예외 처리를 명확하게 표현 Co-Authored-By: Claude Sonnet 4.6 --- .../example/solidconnection/common/exception/ErrorCode.java | 1 + .../solidconnection/siteuser/service/SchoolEmailService.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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 8edb30cd2..8ad4aaa6c 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -83,6 +83,7 @@ public enum ErrorCode { 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(), "학교 이메일 인증 정보 저장에 실패했습니다."), // s3 S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java index 072577a40..d1bbfe2bc 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -1,6 +1,7 @@ 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_VERIFICATION_INFO_SAVE_FAILED; 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; @@ -33,7 +34,6 @@ public class SchoolEmailService { private final SiteUserRepository siteUserRepository; private final HomeUniversityRepository homeUniversityRepository; private final MailService mailService; - @Qualifier("redisTemplate") private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; @@ -80,7 +80,7 @@ private void saveVerificationInfo(long siteUserId, SchoolVerificationInfo info) TimeUnit.SECONDS ); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_SAVE_FAILED); } } From a4aeacaa49d67b15b986042dc586246ec7eb6241 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 22:34:24 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20Redis=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=AD=EC=A7=81=EB=A0=AC=ED=99=94=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20CORRUPTED=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데이터가 존재하지만 파싱 실패인 경우 REQUEST_NOT_FOUND 대신 SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED로 명확하게 구분 Co-Authored-By: Claude Sonnet 4.6 --- .../solidconnection/common/exception/ErrorCode.java | 1 + .../siteuser/service/SchoolEmailService.java | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) 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 8ad4aaa6c..f9095016d 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -84,6 +84,7 @@ public enum ErrorCode { 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 서비스 에러 발생"), diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java index d1bbfe2bc..b9e986b2f 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -1,6 +1,7 @@ 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_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.SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND; @@ -85,15 +86,15 @@ private void saveVerificationInfo(long siteUserId, SchoolVerificationInfo info) } private SchoolVerificationInfo getVerificationInfo(long siteUserId) { - String json = redisTemplate.opsForValue().get(KEY_PREFIX + siteUserId); - if (json == null) { + String jsonInfo = redisTemplate.opsForValue().get(KEY_PREFIX + siteUserId); + if (jsonInfo == null) { throw new CustomException(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND); } try { - return objectMapper.readValue(json, SchoolVerificationInfo.class); + return objectMapper.readValue(jsonInfo, SchoolVerificationInfo.class); } catch (JsonProcessingException e) { redisTemplate.delete(KEY_PREFIX + siteUserId); - throw new CustomException(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND); + throw new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED); } } From ecc320faea542071423aa496bc405aead39f96c7 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 22:51:19 +0900 Subject: [PATCH 06/11] =?UTF-8?q?test:=20=ED=95=99=EA=B5=90=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9D=B8=ED=95=98=EB=8C=80=ED=95=99?= =?UTF-8?q?=EA=B5=90(inha.edu)=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../service/SchoolEmailServiceTest.java | 20 +++++++++---------- .../fixture/HomeUniversityFixture.java | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java index bcdd6a5a4..fda72d8ab 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java @@ -51,23 +51,23 @@ class 학교_이메일_인증_요청 { @Test void 인증_코드가_발급되고_이메일이_발송된다() { // Given - homeUniversityFixture.인천대학교(); + homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.사용자(); // When & Then - schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr"); - then(mailService).should().sendVerificationEmail(eq("test@inu.ac.kr"), any()); + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); + then(mailService).should().sendVerificationEmail(eq("test@inha.edu"), any()); } @Test void 이미_학교_인증된_사용자는_예외가_발생한다() { // Given - HomeUniversity homeUniversity = homeUniversityFixture.인천대학교(); + HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.국내_대학_정보_소지_사용자(homeUniversity.getId()); // When & Then assertThatThrownBy(() -> - schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr")) + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu")) .isInstanceOf(CustomException.class) .hasMessage(SCHOOL_EMAIL_ALREADY_VERIFIED.getMessage()); } @@ -92,9 +92,9 @@ class 학교_이메일_인증_확인 { @Test void 인증_코드가_일치하면_homeUniversityId가_설정되고_인증이_완료된다() { // Given - HomeUniversity homeUniversity = homeUniversityFixture.인천대학교(); + HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.사용자(); - schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr"); + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); then(mailService).should().sendVerificationEmail(any(), codeCaptor.capture()); @@ -109,7 +109,7 @@ class 학교_이메일_인증_확인 { } @Test - void 인증_요청이_없으면_예외가_발생한다() { + void 인증_정보가_없으면_예외가_발생한다() { // Given SiteUser siteUser = siteUserFixture.사용자(); @@ -123,9 +123,9 @@ class 학교_이메일_인증_확인 { @Test void 인증_코드가_다르면_예외가_발생한다() { // Given - homeUniversityFixture.인천대학교(); + homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.사용자(); - schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inu.ac.kr"); + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); // When & Then assertThatThrownBy(() -> 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 3ba59fdcc..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,7 +13,7 @@ public class HomeUniversityFixture { public HomeUniversity 인하대학교() { return homeUniversityFixtureBuilder.homeUniversity() .name("인하대학교") - .emailDomain("inha.ac.kr") + .emailDomain("inha.edu") .create(); } From 7b35be96c4c58856e55266d78230892125416312 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 22:54:47 +0900 Subject: [PATCH 07/11] =?UTF-8?q?style:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20Gi?= =?UTF-8?q?ven-When-Then=20=EC=A3=BC=EC=84=9D=20=EC=86=8C=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../service/SchoolEmailServiceTest.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java index fda72d8ab..2cf4de360 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java @@ -50,22 +50,22 @@ class 학교_이메일_인증_요청 { @Test void 인증_코드가_발급되고_이메일이_발송된다() { - // Given + // given homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.사용자(); - // When & Then + // when & then schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); then(mailService).should().sendVerificationEmail(eq("test@inha.edu"), any()); } @Test void 이미_학교_인증된_사용자는_예외가_발생한다() { - // Given + // given HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.국내_대학_정보_소지_사용자(homeUniversity.getId()); - // When & Then + // when & then assertThatThrownBy(() -> schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu")) .isInstanceOf(CustomException.class) @@ -74,10 +74,10 @@ class 학교_이메일_인증_요청 { @Test void 지원하지_않는_이메일_도메인은_예외가_발생한다() { - // Given + // given SiteUser siteUser = siteUserFixture.사용자(); - // When & Then + // when & then assertThatThrownBy(() -> schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@unknown.ac.kr")) .isInstanceOf(CustomException.class) @@ -91,7 +91,7 @@ class 학교_이메일_인증_확인 { @Test void 인증_코드가_일치하면_homeUniversityId가_설정되고_인증이_완료된다() { - // Given + // given HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.사용자(); schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); @@ -110,10 +110,10 @@ class 학교_이메일_인증_확인 { @Test void 인증_정보가_없으면_예외가_발생한다() { - // Given + // given SiteUser siteUser = siteUserFixture.사용자(); - // When & Then + // when & then assertThatThrownBy(() -> schoolEmailService.confirmSchoolEmail(siteUser.getId(), "123456")) .isInstanceOf(CustomException.class) @@ -122,12 +122,12 @@ class 학교_이메일_인증_확인 { @Test void 인증_코드가_다르면_예외가_발생한다() { - // Given + // given homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.사용자(); schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); - // When & Then + // when & then assertThatThrownBy(() -> schoolEmailService.confirmSchoolEmail(siteUser.getId(), "000000")) .isInstanceOf(CustomException.class) From 1c52ee3c0a5b3b39d0159c8e92f3ee7f9fb640f3 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 23:19:26 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20data.sql=EC=97=90=20HomeUniversit?= =?UTF-8?q?y=20email=5Fdomain=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/main/resources/data.sql | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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, From 1464ba3c550cb12606f5847e1278453f88a2d1dd Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 9 Jun 2026 23:53:37 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20CodeRabbit=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20-=20emailDomain=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B0=95=ED=99=94=20=EB=B0=8F=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EB=B2=94=EC=9C=84=20=EC=B6=95=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - V50 마이그레이션에 기존 대학 email_domain 백필 UPDATE 추가 - AdminHomeUniversityService에 emailDomain 중복 검증 추가 - SchoolEmailService에서 이메일 발송 실패 시 Redis 보상 삭제 추가 - requestSchoolEmailVerification에서 불필요한 @Transactional 제거 - extractEmailDomain에서 도메인 소문자 정규화 적용 - AdminHomeUniversity DTO의 emailDomain 검증을 @Pattern으로 강화 Co-Authored-By: Claude Sonnet 4.6 --- .../dto/AdminHomeUniversityCreateRequest.java | 6 ++++- .../dto/AdminHomeUniversityUpdateRequest.java | 6 ++++- .../service/AdminHomeUniversityService.java | 25 +++++++++++++++++++ .../common/exception/ErrorCode.java | 1 + .../siteuser/service/SchoolEmailService.java | 10 +++++--- ...0__add_email_domain_to_home_university.sql | 6 +++++ 6 files changed, 49 insertions(+), 5 deletions(-) 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 875ee98f0..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,13 +1,17 @@ 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, - @Size(max = 100, message = "이메일 도메인은 100자 이하여야 합니다") + @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/AdminHomeUniversityUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHomeUniversityUpdateRequest.java index 194ebf425..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,13 +1,17 @@ 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, - @Size(max = 100, message = "이메일 도메인은 100자 이하여야 합니다") + @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 906ad2dab..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,6 +49,7 @@ public AdminHomeUniversityResponse getHomeUniversity(Long id) { ) public AdminHomeUniversityResponse createHomeUniversity(AdminHomeUniversityCreateRequest request) { validateNameNotExists(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,6 +81,7 @@ public AdminHomeUniversityResponse updateHomeUniversity(Long id, AdminHomeUniver HomeUniversity homeUniversity = homeUniversityRepository.findById(id) .orElseThrow(() -> new CustomException(HOME_UNIVERSITY_NOT_FOUND)); validateNameNotDuplicated(request.name(), id); + 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 f9095016d..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(), "이름에 해당하는 국가를 찾을 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java index b9e986b2f..4bf969d45 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -38,7 +38,6 @@ public class SchoolEmailService { 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)); @@ -54,7 +53,12 @@ public void requestSchoolEmailVerification(long siteUserId, String schoolEmail) String code = generateVerificationCode(); saveVerificationInfo(siteUserId, new SchoolVerificationInfo(schoolEmail, homeUniversity.getId(), code)); - mailService.sendVerificationEmail(schoolEmail, code); + try { + mailService.sendVerificationEmail(schoolEmail, code); + } catch (Exception e) { + redisTemplate.delete(KEY_PREFIX + siteUserId); + throw e; + } } @Transactional @@ -103,7 +107,7 @@ private String extractEmailDomain(String email) { if (atIndex == -1) { throw new CustomException(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED); } - return email.substring(atIndex + 1); + return email.substring(atIndex + 1).toLowerCase(); } private String generateVerificationCode() { 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 index dde41adc0..249bd5d42 100644 --- 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 @@ -1,2 +1,8 @@ ALTER TABLE home_university ADD COLUMN email_domain VARCHAR(100) NULL UNIQUE; + +UPDATE home_university SET email_domain = 'inha.edu' WHERE name = '인하대학교'; +UPDATE home_university SET email_domain = 'khu.ac.kr' WHERE name = '경희대학교'; +UPDATE home_university SET email_domain = 'cau.ac.kr' WHERE name = '중앙대학교'; +UPDATE home_university SET email_domain = 'sungshin.ac.kr' WHERE name = '성신여자대학교'; +UPDATE home_university SET email_domain = 'inu.ac.kr' WHERE name = '인천대학교'; From 7d475c6a90eff0ead7716758186b1f88049b125b Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Wed, 10 Jun 2026 00:49:21 +0900 Subject: [PATCH 10/11] =?UTF-8?q?revert:=20V50=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=EC=97=90=EC=84=9C=20DML=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../migration/V50__add_email_domain_to_home_university.sql | 6 ------ 1 file changed, 6 deletions(-) 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 index 249bd5d42..dde41adc0 100644 --- 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 @@ -1,8 +1,2 @@ ALTER TABLE home_university ADD COLUMN email_domain VARCHAR(100) NULL UNIQUE; - -UPDATE home_university SET email_domain = 'inha.edu' WHERE name = '인하대학교'; -UPDATE home_university SET email_domain = 'khu.ac.kr' WHERE name = '경희대학교'; -UPDATE home_university SET email_domain = 'cau.ac.kr' WHERE name = '중앙대학교'; -UPDATE home_university SET email_domain = 'sungshin.ac.kr' WHERE name = '성신여자대학교'; -UPDATE home_university SET email_domain = 'inu.ac.kr' WHERE name = '인천대학교'; From 07cb653ca9970e89efa4c76d70202711bd2b7273 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Wed, 10 Jun 2026 15:14:10 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20@Email=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=9E=A5=EB=90=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20atIndex=20=EA=B2=80=EC=82=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../siteuser/service/SchoolEmailService.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java index 4bf969d45..34d22233f 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -1,11 +1,11 @@ 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_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.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; @@ -20,7 +20,6 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +37,7 @@ public class SchoolEmailService { 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)); @@ -103,11 +103,7 @@ private SchoolVerificationInfo getVerificationInfo(long siteUserId) { } private String extractEmailDomain(String email) { - int atIndex = email.indexOf('@'); - if (atIndex == -1) { - throw new CustomException(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED); - } - return email.substring(atIndex + 1).toLowerCase(); + return email.substring(email.indexOf('@') + 1).toLowerCase(); } private String generateVerificationCode() {