Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b7d8c03
merge: Log 파일 관련 설정 및 CI/CD 파이프라인 개선점 적용
snowykte0426 Feb 8, 2025
b0d5e75
add: EmailVerification 도메인 모델 추가
se0hui Feb 13, 2025
51ce29f
add: 이메일 인증 관련 예외 처리 추가
se0hui Feb 13, 2025
79942e1
add: JPA 기반 EmailVerificationRepository 구현
se0hui Feb 13, 2025
f02ecdb
add: Spring Data 기반 EmailVerificationRepository 추가
se0hui Feb 13, 2025
1a5653d
add: EmailVerificationPort 인터페이스 추가
se0hui Feb 13, 2025
9065c9c
add: 이메일 인증 서비스 구현
se0hui Feb 13, 2025
39cf97c
add: 이메일 인증 UseCase
se0hui Feb 13, 2025
77ee375
add: 이메일 설정을 위한 Config
se0hui Feb 13, 2025
6c672d0
add: AuthController에 이메일 인증 API 추가
se0hui Feb 13, 2025
7c54617
update: 인증 코드 생성 범위 수정
se0hui Feb 14, 2025
778d8c6
test: EmailVerificationservice, AuthController test 추가
se0hui Feb 14, 2025
2037fec
delete: unused import 삭제
se0hui Feb 14, 2025
79f27a5
update: 이메일 관련 기능 엔드포인트 변경
se0hui Feb 14, 2025
a856bfc
test: 변경된 엔드포인드로 수정
se0hui Feb 14, 2025
b2f0b14
update: 이메일 인증 엔티티 생성 및 테이블 이름을 'email'로 지정
se0hui Feb 14, 2025
95bd04f
Merge remote-tracking branch 'origin/develop' into feature/email-api
se0hui Feb 14, 2025
9dab2e1
update: @RequiredArgsConstructor 적용하여 생성자 코드 간소화
se0hui Feb 14, 2025
7787679
update: 인증 코드 생성 부분 주석 수정
se0hui Feb 14, 2025
028e202
update: UseCaseTransaction 어노테이션 및 RequiredArgsConstructor 어노테이션 적용
se0hui Feb 14, 2025
7b9c962
update: RequiredArgsConstructor 어노테이션 적용
se0hui Feb 14, 2025
c520a2c
update: 변경된 엔드포인트로 수정
se0hui Feb 14, 2025
334075e
update: 하드코딩된 숫자 상수화
se0hui Feb 14, 2025
61dc311
test: 변경된 엔드포인트로 수정
se0hui Feb 14, 2025
5200658
add: AuthException을 구체적인 예외 클래스로 분리
se0hui Feb 15, 2025
e49d0e0
add: 오류 추가
se0hui Feb 15, 2025
8fb525f
delete: AuthException 삭제
se0hui Feb 16, 2025
2b42387
update: persistence 계층 이동 및 @setter 제거
se0hui Feb 16, 2025
9699ed3
add: EmailVerificationAdapter 추가
se0hui Feb 16, 2025
78070cf
update: port 상속 제거
se0hui Feb 16, 2025
88ba256
update: @Transactional 제거
se0hui Feb 16, 2025
6ab30b2
chore: EmailVerification의 계층 이동에 따른 update
se0hui Feb 16, 2025
cc63744
add: EmailRequest, VerificationCodeRequest 정의
se0hui Feb 16, 2025
4057cd6
add: EmailSchedulingService 생성
se0hui Feb 16, 2025
f32e1c9
add: @Valid 어노테이션 추가 및 검증 로직 Application 계층으로 이동
se0hui Feb 16, 2025
f3af5d7
delete: 사용하지 않는 테스트 코드 제거
se0hui Feb 16, 2025
b8088ba
add: EmailVerificationUseCase에 대한 test 코드 작성
se0hui Feb 16, 2025
0bcc57c
update: email 및 verificationCode 검증 코드 수정
se0hui Feb 16, 2025
80b7d29
update: 이메일 인증 로직 개선
se0hui Feb 16, 2025
afe7b1a
test: test 코드 수정
se0hui Feb 16, 2025
8947267
add: application-test.yml 이메일 설정 추가
se0hui Feb 16, 2025
b8decdf
chore: application-test.yml
se0hui Feb 16, 2025
6287c15
chore: package 변경
se0hui Feb 17, 2025
3028cb3
delete: 사용하지 않는 Exception 클래스
se0hui Feb 17, 2025
9571324
add: 예외 명칭 세분화
se0hui Feb 17, 2025
bbbd82d
update: 예외 명칭 변경에 따른 코드 수정
se0hui Feb 17, 2025
13f9762
chore: ErrorCode
se0hui Feb 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.ampersand.groom.domain.auth.application.port;

import com.ampersand.groom.domain.auth.persistence.EmailVerification;

import java.time.LocalDateTime;
import java.util.Optional;

public interface EmailVerificationPort {

// 인증 정보 저장
EmailVerification save(EmailVerification emailVerification);

// 인증 코드로 이메일 조회
Optional<EmailVerification> findByCode(String code);

// 이메일로 인증 정보 조회
Optional<EmailVerification> findByEmail(String email);

// 만료된 인증 정보 삭제
void deleteAllExpired(LocalDateTime now);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.ampersand.groom.domain.auth.application.service;

import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
public class EmailSchedulingService {

private final EmailVerificationPort emailVerificationPort;

// 만료된 인증 정보 삭제(1시간)
@Scheduled(fixedRate = 3600000)
@Transactional
public void deleteExpiredVerifications() {
emailVerificationPort.deleteAllExpired(LocalDateTime.now());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.ampersand.groom.domain.auth.application.service;

import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort;
import com.ampersand.groom.domain.auth.expection.EmailFormatInvalidException;
import com.ampersand.groom.domain.auth.expection.VerificationCodeFormatInvalidException;
import com.ampersand.groom.domain.auth.expection.VerificationCodeExpiredOrInvalidException;
import com.ampersand.groom.domain.auth.persistence.EmailVerification;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
@RequiredArgsConstructor
public class EmailVerificationService {

private final EmailVerificationPort emailVerificationPort;
private final JavaMailSender javaMailSender;

private static final int MAX_EMAIL_LENGTH = 16;
private static final int CODE_LENGTH = 8;


//8자리 숫자 인증 코드 생성
private String generateVerificationCode() {
Random random = new Random();
int code = 10000000 + random.nextInt(90000000);
return String.valueOf(code);
}

// 이메일 전송 메서드
private void sendEmail(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(text);
javaMailSender.send(message);
}

// 회원가입 인증 이메일 전송
public void sendSignupVerificationEmail(String email) {
verifyEmail(email);
String code = generateVerificationCode();
sendEmail(email, "회원가입 인증", "귀하의 인증 코드는: " + code);

EmailVerification emailVerification = new EmailVerification(email, code);

emailVerificationPort.save(emailVerification);
}

// 비밀번호 변경을 위한 인증 이메일 전송
public void sendPasswordResetEmail(String email) {
verifyEmail(email);
String code = generateVerificationCode();
sendEmail(email, "비밀번호 변경 인증", "귀하의 인증 코드는: " + code);

EmailVerification emailVerification = new EmailVerification(email, code);

emailVerificationPort.save(emailVerification);
}

// 인증 코드 검증
public void verifyCode(String code) {
if(code == null || code.length() != CODE_LENGTH) {
throw new VerificationCodeFormatInvalidException();
}

EmailVerification emailVerification = emailVerificationPort.findByCode(code)
.orElseThrow(VerificationCodeExpiredOrInvalidException::new);


emailVerification.setIsVerified(true);
emailVerificationPort.save(emailVerification);
}

// 이메일 검증
public void verifyEmail(String email) {
if(email == null || email.length() != MAX_EMAIL_LENGTH) {
throw new EmailFormatInvalidException();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ampersand.groom.domain.auth.application.usecase;

import com.ampersand.groom.domain.auth.application.service.EmailVerificationService;
import com.ampersand.groom.global.annotation.usecase.UseCaseWithTransaction;
import lombok.RequiredArgsConstructor;

@UseCaseWithTransaction
@RequiredArgsConstructor
public class EmailVerificationUseCase {

private final EmailVerificationService emailVerificationService;


// 회원가입 인증 이메일 전송
public void executeSendSignupVerificationEmail(String email) {
emailVerificationService.sendSignupVerificationEmail(email);
}

// 비밀번호 변경 인증 이메일 전송
public void executeSendPasswordResetEmail(String email) {
emailVerificationService.sendPasswordResetEmail(email);
}

// 인증 코드 검증
public void executeVerifyCode(String code) {
emailVerificationService.verifyCode(code);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class EmailFormatInvalidException extends GroomException {
public EmailFormatInvalidException() {
super(ErrorCode.EMAIL_FORMAT_INVALID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class VerificationCodeExpiredOrInvalidException extends GroomException {
public VerificationCodeExpiredOrInvalidException() {
super(ErrorCode.VERIFICATION_CODE_EXPIRED_OR_INVALID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class VerificationCodeFormatInvalidException extends GroomException {
public VerificationCodeFormatInvalidException() {
super(ErrorCode.VERIFICATION_CODE_FORMAT_INVALID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.ampersand.groom.domain.auth.persistence;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@Table(name = "email")
@ToString
public class EmailVerification {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String code;
@Column(nullable = false)
private boolean isVerified;
@Column(nullable = false)
private LocalDateTime verificationDate;

public EmailVerification(String email, String code) {
this.email = email;
this.code = code;
this.isVerified = false;
this.verificationDate = LocalDateTime.now().plusMinutes(5);
}


@Builder
public EmailVerification(Long id, String email, String code, boolean isVerified, LocalDateTime verificationDate) {
this.id = id;
this.email = email;
this.code = code;
this.isVerified = isVerified;
this.verificationDate = verificationDate;
}


public void setIsVerified(boolean isVerified) {
this.isVerified = isVerified;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.ampersand.groom.domain.auth.persistence.adapter.email;

import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort;

import com.ampersand.groom.domain.auth.persistence.EmailVerification;
import com.ampersand.groom.domain.auth.persistence.repository.JpaEmailVerificationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Optional;

@Component
@RequiredArgsConstructor
public class EmailVerificationAdapter implements EmailVerificationPort {

private final JpaEmailVerificationRepository jpaEmailVerificationRepository;

@Override
public EmailVerification save(EmailVerification emailVerification) {
return jpaEmailVerificationRepository.save(emailVerification);
}

@Override
public Optional<EmailVerification> findByCode(String code) {
return jpaEmailVerificationRepository.findByCode(code);
}

@Override
public Optional<EmailVerification> findByEmail(String email) {
return jpaEmailVerificationRepository.findByEmail(email);
}

@Override
public void deleteAllExpired(LocalDateTime now) {
jpaEmailVerificationRepository.deleteAllExpired(now);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.ampersand.groom.domain.auth.persistence.repository;

import com.ampersand.groom.domain.auth.persistence.EmailVerification;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.Optional;

@Repository
@RequiredArgsConstructor
public class JpaEmailVerificationRepository {

private final SpringDataEmailVerificationRepository repository;


public EmailVerification save(EmailVerification emailVerification) {
return repository.save(emailVerification);
}

public Optional<EmailVerification> findByCode(String code) {
return repository.findByCode(code);
}

public Optional<EmailVerification> findByEmail(String email) {
return repository.findByEmail(email);
}

public void deleteAllExpired(LocalDateTime now) {
repository.deleteAllExpired(now);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.ampersand.groom.domain.auth.persistence.repository;

import com.ampersand.groom.domain.auth.persistence.EmailVerification;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.Optional;

@Repository
public interface SpringDataEmailVerificationRepository extends JpaRepository<EmailVerification, Long> {

Optional<EmailVerification> findByCode(String code);

Optional<EmailVerification> findByEmail(String email);

@Modifying
@Query("DELETE FROM EmailVerification e WHERE e.verificationDate < :now")
void deleteAllExpired(@Param("now") LocalDateTime now);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.ampersand.groom.domain.auth.presentation.controller;

import com.ampersand.groom.domain.auth.application.usecase.EmailVerificationUseCase;
import com.ampersand.groom.domain.auth.presentation.dto.EmailRequest;
import com.ampersand.groom.domain.auth.presentation.dto.VerificationCodeRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RestController;


@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

private final EmailVerificationUseCase emailVerificationUseCase;

@PostMapping("/verify-email")
public ResponseEntity<?> verifyEmail(@RequestBody @Valid VerificationCodeRequest request) {
emailVerificationUseCase.executeVerifyCode(request.getCode());
return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification successful.");
}

@PostMapping("/signup/email")
public ResponseEntity<?> signup(@RequestBody @Valid EmailRequest request) {
emailVerificationUseCase.executeSendSignupVerificationEmail(request.getEmail());
return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification email sent");
}

@PostMapping("/password-change/email")
public ResponseEntity<?> refresh(@RequestBody @Valid EmailRequest request) {
emailVerificationUseCase.executeSendPasswordResetEmail(request.getEmail());
return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification email sent");
}
}
Loading