@Column(nullable = false)
private boolean emailVerified = false;
// 이메일 인증 상태 설정
public void changeEmailVerified(boolean emailVerified) {
this.emailVerified = emailVerified;
}
사용자의 학교 이메일 인증이 완료 되었는지 확인하기 위한 필드와 인증 상태를 설정하는 메서드를 추가해준다.
// 상품 등록
@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("학교 이메일 인증이 필요합니다.");
}
}
상품 등록을 할 때 학교 이메일이 인증된 사용자만 등록할 수 있게 한다.
@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);
}
}
public interface EmailVerificationRepository extends JpaRepository<EmailVerification, Long> {
Optional<EmailVerification> findByEmail(String email);
}
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
) {}
}
@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: 인증 이메일 발송 메서드verifyEmail: 인증 코드 검증 메서드
verifyEmail 메서드는 두 개의 이메일 매개변수를 받는다.
userEmail: 사용자 계정의 이메일request.email(): 인증할 학교 이메일@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" // 이메일로 받은 인증 코드
}

상품 등록
