-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 학교 이메일 인증으로 HomeUniversity 자동 매핑 #752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
09e40cf
a9cf908
7ea551b
0b9ddd1
a4aeaca
ecc320f
7b35be9
1c52ee3
1464ba3
7d475c6
07cb653
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.example.solidconnection.siteuser.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record SchoolEmailConfirmRequest( | ||
| @NotBlank(message = "인증 코드는 필수입니다") | ||
| String code | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, String> redisTemplate; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| @Transactional | ||
| public void requestSchoolEmailVerification(long siteUserId, String schoolEmail) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 트랜잭션 어노테이션 달아주세요 ! |
||
| 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DTO에서 이미
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 중복 검증이네요 바로 substring으로 잘라도 무방할것같습니다 수정하겠음돠 |
||
| return email.substring(email.indexOf('@') + 1).toLowerCase(); | ||
| } | ||
|
Comment on lines
+105
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이메일 형식 검증 강화 필요 현재 개선 방안:
🤖 Prompt for AI Agents |
||
|
|
||
| private String generateVerificationCode() { | ||
| return String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000)); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.