학교 이메일 인증

뚜우웅이·2025년 4월 20일

캡스톤 디자인

목록 보기
14/35

User


    @Column(nullable = false)
    private boolean emailVerified = false;
    
    // 이메일 인증 상태 설정
    public void changeEmailVerified(boolean emailVerified) {
        this.emailVerified = emailVerified;
    }

사용자의 학교 이메일 인증이 완료 되었는지 확인하기 위한 필드와 인증 상태를 설정하는 메서드를 추가해준다.

Product

Service


    // 상품 등록
    @Transactional
    public ProductDto.ProductResponse createProduct(Long userId, ProductDto.CreateRequest request, List<MultipartFile> images) throws BadRequestException {
        User seller = userRepository.findById(userId)
                .orElseThrow(() -> new UserException.UserNotFoundException(userId));

        // 학교 이메일 인증 여부 확인
        emailVerification(seller);

        Product product = Product.builder()
                .name(request.name())
                .description(request.description())
                .price(request.price())
                .stock(request.stock())
                .category(request.category())
                .seller(seller)
                .build();

        List<FileStorageService.FileUploadResult> uploadedFiles = new ArrayList<>();
        try {
            if (images != null && !images.isEmpty()) {
                for (int i = 0; i < images.size(); i++) {
                    MultipartFile imageFile = images.get(i);
                    FileStorageService.FileUploadResult result = fileStorageService.storeFile(imageFile);
                    uploadedFiles.add(result);
                    boolean isThumbnail = (i == 0); // 첫 번째 이미지 -> 썸네일
                    ProductImage productImage = ProductImage.builder()
                            .imageUrl(result.originalFilePath())
                            .thumbnailUrl(result.thumbnailFilePath())
                            .originalFileName(result.originalFileName())
                            .displayOrder(i)
                            .isThumbnail(isThumbnail)
                            .build();
                    product.addImage(productImage); // Product 와 ProductImage 연결
                }
            }
            Product savedProduct = productRepository.save(product);
            log.info("상품 등록 완료: 상품명 {}, 판매자 ID {}", request.name(), userId);
            return ProductDto.ProductResponse.from(savedProduct);
        } catch (Exception e) {
            log.error("상품 등록 중 오류 발생: 사용자 ID {}", userId, e);
            uploadedFiles.forEach(uploadedFile -> {
                fileStorageService.deleteFile(uploadedFile.originalFilePath());
                fileStorageService.deleteFile(uploadedFile.thumbnailFilePath());
            });
            throw new RuntimeException("상품 등록 중 오류가 발생했습니다.", e);
        }
    }
    
    ----
    private static void emailVerification(User seller) throws BadRequestException {
        if (!seller.isEmailVerified()) {
            throw new BadRequestException("학교 이메일 인증이 필요합니다.");
        }
    }

상품 등록을 할 때 학교 이메일이 인증된 사용자만 등록할 수 있게 한다.

EmailVerification

Entity

@Entity
@Table(name = "email_verification")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class EmailVerification extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String verificationCode;

    @Column(nullable = false)
    private LocalDateTime expiryDate;

    @Column(nullable = false)
    private boolean verified;

    @Builder
    public EmailVerification(String email, String verificationCode, LocalDateTime expiryDate, boolean verified) {
        this.email = email;
        this.verificationCode = verificationCode;
        this.expiryDate = expiryDate;
        this.verified = false;
    }

    public void verify() {
        this.verified = true;
    }
    public boolean isExpired() {
        return LocalDateTime.now().isAfter(this.expiryDate);
    }
}
  • 사용자가 인증을 요청하면 인증 코드가 생성되어 이 엔터티에 저장된다.

Repository

public interface EmailVerificationRepository extends JpaRepository<EmailVerification, Long> {
    Optional<EmailVerification> findByEmail(String email);
}

DTO

public class EmailVerificationDto {

    // 이메일 인증 요청
    public record EmailVerificationRequest(
           @NotBlank(message = "이메일은 필수 입력값입니다.")
           @Email(message = "이메일 형식이 올바르지 않습니다.")
           String email
    ) {}

    // 이메일 인증 코드 확인 요청
    public record VerificationCodeRequest(
            @NotBlank(message = "이메일은 필수 입력값입니다.")
            @Email(message = "이메일 형식이 올바르지 않습니다.")
            String email,

            @NotBlank(message = "인증 코드는 필수 입력값입니다.")
            String verificationCode
    ) {}

    // 이메일 인증 응답
    public record EmailVerificationResponse(
            boolean success,
            String message
    ) {}
}
  • 학교 이메일을 입력 받아서 인증 코드를 받고, 인증 시에는 학교 이메일과 인증 코드로 인증을 진행하기 위한 DTO다.

Service

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class EmailVerificationService {

    private final EmailVerificationRepository emailVerificationRepository;
    private final UserRepository userRepository;
    private final JavaMailSender mailSender;

    private static final String ALLOWED_DOMAINS = "office.skhu.ac.kr";
    private static final int CODE_LENGTH = 6;
    private static final int EXPIRY_MINUTES = 10;

    @Transactional
    public EmailVerificationDto.EmailVerificationResponse sendVerificationEmail(EmailVerificationDto.EmailVerificationRequest request) throws BadRequestException {
        String email = request.email();

        // 학교 이메일 도메인 확인
        if (!isSchoolEmail(email)) {
            throw new BadRequestException("학교 이메일만 사용할 수 있습니다.");
        }

        // 인증 코드 생성
        String verificationCode = generateVerificationCode();

        // 이전 인증 코드 만료 처리
        emailVerificationRepository.findByEmail(email)
                .ifPresent(emailVerificationRepository::delete);

        // 새 인증 코드 저장
        EmailVerification emailVerification = EmailVerification.builder()
                .email(email)
                .verificationCode(verificationCode)
                .expiryDate(LocalDateTime.now().plusMinutes(EXPIRY_MINUTES))
                .build();

        emailVerificationRepository.save(emailVerification);

        // 이메일 발송
        sendEmail(email, "이메일 인증 코드", "인증 코드: " + verificationCode + "\n\n이 코드는 " + EXPIRY_MINUTES + "분 후에 만료됩니다.");

        return new EmailVerificationDto.EmailVerificationResponse(true, "인증 이메일이 발송되었습니다.");
    }

    @Transactional
    public EmailVerificationDto.EmailVerificationResponse verifyEmail(String userEmail, EmailVerificationDto.VerificationCodeRequest request) throws BadRequestException {
        String email = request.email();
        String code = request.verificationCode();

        EmailVerification verification = emailVerificationRepository.findByEmail(email)
                .orElseThrow(() -> new BadRequestException("인증 정보를 찾을 수 업습니다."));

        if (verification.isExpired()) {
            throw new BadRequestException("인증 코드가 만료되었습니다.");
        }

        if (!verification.getVerificationCode().equals(code)) {
            throw new BadRequestException("인증 코드가 일치하지 않습니다.");
        }

        verification.verify();
        emailVerificationRepository.save(verification);

        // 사용자의 이메일 인증 상태 업데이트
        Optional<User> userOpt = userRepository.findByEmail(userEmail);
        if (userOpt.isPresent()) {
            User user = userOpt.get();
            user.changeEmailVerified(true);
            userRepository.save(user);
        }

        return new EmailVerificationDto.EmailVerificationResponse(true, "이메일 인증이 완료되었습니다.");
    }

    private boolean isSchoolEmail(String email) {
        return email.toLowerCase().endsWith("@" + ALLOWED_DOMAINS);
    }

    private String generateVerificationCode() {
        SecureRandom random = new SecureRandom();
        StringBuilder code = new StringBuilder();

        for (int i = 0; i < CODE_LENGTH; i++) {
            code.append(random.nextInt(10));
        }

        return code.toString();
    }

    private void sendEmail(String to, String subject, String text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);
        mailSender.send(message);
    }
}

주요 메서드

  • sendVerificationEmail: 인증 이메일 발송 메서드
    • 학교 이메일 도메인 검증
    • 6자리 인증 코드 생성
    • 이전 인증 코드가 있으면 삭제
    • 새 인증 코드 생성 및 저장
    • 인증 이메일 발송
  • verifyEmail: 인증 코드 검증 메서드

    • 입력받은 이메일과 코드로 인증 정보 조회
    • 인증 코드 만료 여부 확인
    • 인증 코드 일치 여부 확인
    • 인증 성공 시 인증 상태 업데이트
    • 사용자(User) 엔티티에 인증 상태 반영
  • verifyEmail 메서드는 두 개의 이메일 매개변수를 받는다.

    • userEmail: 사용자 계정의 이메일
    • request.email(): 인증할 학교 이메일

Controller

@RestController
@RequestMapping("/api/auth/email-verification")
@RequiredArgsConstructor
@Tag(name = "Email Verification", description = "이메일 인증 API")
public class EmailVerificationController {

    private final EmailVerificationService emailVerificationService;

    @PostMapping("/send")
    @Operation(summary = "인증 이메일 발송", description = "학교 이메일로 인증 코드를 발송합니다.")
    public ResponseEntity<ResponseDTO<EmailVerificationDto.EmailVerificationResponse>> sendVerificationEmail(
            @Parameter(description = "이메일")
            @Valid @RequestBody EmailVerificationDto.EmailVerificationRequest request) throws BadRequestException {

        EmailVerificationDto.EmailVerificationResponse response = emailVerificationService.sendVerificationEmail(request);
        return ResponseEntity.ok(ResponseDTO.success(response));
    }

    @PostMapping("/verify")
    @Operation(summary = "인증 코드 확인", description = "발송된 인증 코드를 확인합니다.")
    public ResponseEntity<ResponseDTO<EmailVerificationDto.EmailVerificationResponse>> verifyEmail(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
            @Parameter(description = "이메일과 인증 코드 정보")
            @Valid @RequestBody EmailVerificationDto.VerificationCodeRequest request) throws BadRequestException {

        EmailVerificationDto.EmailVerificationResponse response = emailVerificationService.verifyEmail(userDetails.getEmail(), request);
        return ResponseEntity.ok(ResponseDTO.success(response));
    }
}
  • 인증 코드 확인 단계에서 로그인한 사용자의 이메일과 인증하려는 이메일을 모두 서비스에 전달한다.
  • 이를 통해 사용자가 자신의 계정 이메일과 다른 학교 이메일을 인증할 수 있다.
  • 응답은 ResponseDTO 형식으로 일관되게 반환된다.

테스트

인증 메일 전송
http://localhost:8080/api/auth/email-verification/send

{
  "email": "학생이메일@office.skhu.ac.kr"
}


메일 이미지

학교 메일 인증
http://localhost:8080/api/auth/email-verification/verify

{
  "email": "학생이메일@office.skhu.ac.kr",
  "verificationCode": "123456" // 이메일로 받은 인증 코드
}

상품 등록

profile
공부하는 초보 개발자

0개의 댓글