하석형 하석형 05-21
250521 하석형 이메일 인증 기능 구현 중
@aeb9444d95db4eeae2a24d3c3198d0c98e4bebf4
build.gradle
--- build.gradle
+++ build.gradle
@@ -81,6 +81,8 @@
 
     // 에디터 태그 제거용 라이브러리
     implementation 'org.jsoup:jsoup:1.19.1'
+    // Gmail SMTP
+    implementation 'org.springframework.boot:spring-boot-starter-mail'
 
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testImplementation 'org.springframework.security:spring-security-test'
src/main/java/com/takensoft/common/config/RedisConfig.java
--- src/main/java/com/takensoft/common/config/RedisConfig.java
+++ src/main/java/com/takensoft/common/config/RedisConfig.java
@@ -6,6 +6,7 @@
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
 /**
@@ -38,5 +39,15 @@
         redisTemp.setValueSerializer(new StringRedisSerializer());
         return redisTemp;
     }
+    @Bean
+    public RedisTemplate<String, Object> redisTemplateObject(RedisConnectionFactory redisConnectionFactory) {
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
+        return redisTemplate;
+    }
 
 }
 
src/main/java/com/takensoft/common/exception/CustomEmailCodeNotMatchException.java (added)
+++ src/main/java/com/takensoft/common/exception/CustomEmailCodeNotMatchException.java
@@ -0,0 +1,26 @@
+package com.takensoft.common.exception;
+
+/**
+ * @author takensoft
+ * @since 2025.05.21
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.21  |    하석형     | 최초 등록
+ *
+ * RuntimeException - 실행 중 발생하는 예외를 처리하는 기본 클래스
+ *
+ * 이메일 인증코드 불일치 시 발생하는 예외
+ */
+public class CustomEmailCodeNotMatchException extends RuntimeException {
+
+    public CustomEmailCodeNotMatchException() {
+
+    }
+    public CustomEmailCodeNotMatchException(String message) {
+        super(message);
+    }
+
+    public CustomEmailCodeNotMatchException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
 
src/main/java/com/takensoft/common/exception/CustomEmailSendFailException.java (added)
+++ src/main/java/com/takensoft/common/exception/CustomEmailSendFailException.java
@@ -0,0 +1,26 @@
+package com.takensoft.common.exception;
+
+/**
+ * @author takensoft
+ * @since 2025.05.21
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.21  |    하석형     | 최초 등록
+ *
+ * RuntimeException - 실행 중 발생하는 예외를 처리하는 기본 클래스
+ *
+ * 이메일 발송 실패 시 발생하는 예외
+ */
+public class CustomEmailSendFailException extends RuntimeException {
+
+    public CustomEmailSendFailException() {
+
+    }
+    public CustomEmailSendFailException(String message) {
+        super(message);
+    }
+
+    public CustomEmailSendFailException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
 
src/main/java/com/takensoft/common/exception/CustomEmailVerifyExpireException.java (added)
+++ src/main/java/com/takensoft/common/exception/CustomEmailVerifyExpireException.java
@@ -0,0 +1,26 @@
+package com.takensoft.common.exception;
+
+/**
+ * @author takensoft
+ * @since 2025.05.21
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.21  |    하석형     | 최초 등록
+ *
+ * RuntimeException - 실행 중 발생하는 예외를 처리하는 기본 클래스
+ *
+ * 이메일 인증시간 만료 시 발생하는 예외
+ */
+public class CustomEmailVerifyExpireException extends RuntimeException {
+
+    public CustomEmailVerifyExpireException() {
+
+    }
+    public CustomEmailVerifyExpireException(String message) {
+        super(message);
+    }
+
+    public CustomEmailVerifyExpireException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
 
src/main/java/com/takensoft/common/exception/CustomEmailVerifyFailException.java (added)
+++ src/main/java/com/takensoft/common/exception/CustomEmailVerifyFailException.java
@@ -0,0 +1,26 @@
+package com.takensoft.common.exception;
+
+/**
+ * @author takensoft
+ * @since 2025.05.21
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.21  |    하석형     | 최초 등록
+ *
+ * RuntimeException - 실행 중 발생하는 예외를 처리하는 기본 클래스
+ *
+ * 이메일 인증 실패 시 발생하는 예외
+ */
+public class CustomEmailVerifyFailException extends RuntimeException {
+
+    public CustomEmailVerifyFailException() {
+
+    }
+    public CustomEmailVerifyFailException(String message) {
+        super(message);
+    }
+
+    public CustomEmailVerifyFailException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
src/main/java/com/takensoft/common/exception/GlobalExceptionHandler.java
--- src/main/java/com/takensoft/common/exception/GlobalExceptionHandler.java
+++ src/main/java/com/takensoft/common/exception/GlobalExceptionHandler.java
@@ -324,6 +324,54 @@
     }
 
     /**
+     * @param cesfe - CustomEmailSendFailException 예외 객체
+     * @return CustomEmailSendFailException에 대한 HTTP 응답
+     *
+     * CustomEmailSendFailException이 발생한 경우
+     */
+    @ExceptionHandler(CustomEmailSendFailException.class)
+    public ResponseEntity<?> handleCustomEmailSendFailException(CustomEmailSendFailException cesfe) {
+        logError(cesfe);
+        return resUtil.errorRes(MessageCode.EMAIL_SEND_FAIL);
+    }
+
+    /**
+     * @param cevee - CustomEmailVerifyExpireException 예외 객체
+     * @return CustomEmailVerifyExpireException에 대한 HTTP 응답
+     *
+     * CustomEmailVerifyExpireException이 발생한 경우
+     */
+    @ExceptionHandler(CustomEmailVerifyExpireException.class)
+    public ResponseEntity<?> handleCustomEmailVerifyExpireException(CustomEmailVerifyExpireException cevee) {
+        logError(cevee);
+        return resUtil.errorRes(MessageCode.EMAIL_VERIFY_EXPIRED);
+    }
+
+    /**
+     * @param cevfe - CustomEmailVerifyFailException 예외 객체
+     * @return CustomEmailVerifyFailException에 대한 HTTP 응답
+     *
+     * CustomEmailVerifyFailException이 발생한 경우
+     */
+    @ExceptionHandler(CustomEmailVerifyFailException.class)
+    public ResponseEntity<?> handleCustomEmailVerifyFailException(CustomEmailVerifyFailException cevfe) {
+        logError(cevfe);
+        return resUtil.errorRes(MessageCode.EMAIL_VERIFY_FAIL);
+    }
+
+    /**
+     * @param cecnme - CustomEmailCodeNotMatchException 예외 객체
+     * @return CustomEmailCodeNotMatchException에 대한 HTTP 응답
+     *
+     * CustomEmailCodeNotMatchException이 발생한 경우
+     */
+    @ExceptionHandler(CustomEmailCodeNotMatchException.class)
+    public ResponseEntity<?> handleCustomEmailCodeNotMatchException(CustomEmailCodeNotMatchException cecnme) {
+        logError(cecnme);
+        return resUtil.errorRes(MessageCode.CODE_NOT_MATCH);
+    }
+
+    /**
      * @param e - Exception 예외 객체
      * @return 기타 예외에 대한 HTTP 응답
      *
src/main/java/com/takensoft/common/message/MessageCode.java
--- src/main/java/com/takensoft/common/message/MessageCode.java
+++ src/main/java/com/takensoft/common/message/MessageCode.java
@@ -66,7 +66,14 @@
     LOGOUT_SUCCESS("user.logout.success", HttpStatus.OK), // 로그아웃 성공
 
     // 파일 관련
-    FILE_UPLOAD_FAIL("file.upload_fail", HttpStatus.INTERNAL_SERVER_ERROR); // 파일 업로드 실패
+    FILE_UPLOAD_FAIL("file.upload_fail", HttpStatus.INTERNAL_SERVER_ERROR), // 파일 업로드 실패
+
+    // 이메일 인증 관련
+    EMAIL_SEND_FAIL("email.send_fail", HttpStatus.INTERNAL_SERVER_ERROR), // 이메일 발송 실패
+    EMAIL_VERIFY_SUCCESS("email.verify_success", HttpStatus.OK), // 이메일 인증 성공
+    EMAIL_VERIFY_EXPIRED("email.verify_expired", HttpStatus.UNAUTHORIZED), // 이메일 인증 만료
+    EMAIL_VERIFY_FAIL("email.verify_fail", HttpStatus.UNAUTHORIZED), // 이메일 인증 실패
+    CODE_NOT_MATCH("email.code_not_match", HttpStatus.UNAUTHORIZED); // 인증 코드 불일치
 
     private final String messageKey;   //  메시지
     private final HttpStatus status;    // HTTP 상태
 
src/main/java/com/takensoft/common/verify/dao/EmailDAO.java (added)
+++ src/main/java/com/takensoft/common/verify/dao/EmailDAO.java
@@ -0,0 +1,19 @@
+package com.takensoft.common.verify.dao;
+
+import com.takensoft.common.verify.vo.EmailVO;
+import org.egovframe.rte.psl.dataaccess.mapper.Mapper;
+
+import java.util.HashMap;
+import java.util.List;
+/**
+ * @author  : 하석형
+ * @since   : 2025.05.20
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.20  |    하석형     | 최초 등록
+ *
+ * 이메일 관련 Mapper
+ */
+@Mapper("emailDAO")
+public interface EmailDAO {
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/common/verify/service/EmailService.java (added)
+++ src/main/java/com/takensoft/common/verify/service/EmailService.java
@@ -0,0 +1,33 @@
+package com.takensoft.common.verify.service;
+
+import com.takensoft.common.verify.vo.EmailVO;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.List;
+/**
+ * @author 하석형
+ * @since 2025.05.20
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.20  |    하석형     | 최초 등록
+ *
+ * 이메일 관련 인터페이스
+ */
+public interface EmailService {
+    /**
+     * @param emailVO - 이메일 정보
+     * @return boolean - 이메일 인증코드 발송 결과
+     *
+     * 이메일 인증코드 발송
+     */
+    public boolean sendEmailVerifyCode(EmailVO emailVO);
+
+    /**
+     * @param emailVO - 이메일 정보
+     * @return boolean - 이메일 인증코드 확인 결과
+     *
+     * 이메일 인증코드 확인
+     */
+    public boolean checkEmailVerifyCode(EmailVO emailVO);
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/common/verify/service/Impl/EmailServiceImpl.java (added)
+++ src/main/java/com/takensoft/common/verify/service/Impl/EmailServiceImpl.java
@@ -0,0 +1,148 @@
+package com.takensoft.common.verify.service.Impl;
+
+import com.takensoft.common.exception.*;
+import com.takensoft.common.util.JWTUtil;
+import com.takensoft.common.verify.dao.EmailDAO;
+import com.takensoft.common.verify.service.EmailService;
+import com.takensoft.common.verify.vo.EmailVO;
+import lombok.RequiredArgsConstructor;
+import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+
+/**
+ * @author 하석형
+ * @since 2025.05.20
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.20  |    하석형     | 최초 등록
+ *
+ * EgovAbstractServiceImpl : 전자정부 상속
+ * EmailService : 이메일 관련 인터페이스 상속
+ * 
+ * 이메일 관련 인터페이스 구현체
+ */
+@Service("emailService")
+@RequiredArgsConstructor
+public class EmailServiceImpl extends EgovAbstractServiceImpl implements EmailService {
+
+    private final EmailDAO emailDAO;
+    private final JWTUtil jwtUtil;
+    private final JavaMailSender mailSender;
+    @Qualifier("redisTemplateObject")
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    @Value("${spring.mail.verifyTime}")
+    private long verifyTime; // 인증코드 유효시간
+
+    @Value("${spring.mail.storeTime}")
+    private long storeTime; // 인증코드 저장시간
+
+    /**
+     * @param emailVO - 이메일 정보
+     * @return boolean - 이메일 인증코드 발송 결과
+     * @throws CustomEmailSendFailException - 이메일 발송 실패 시
+     * @throws DataAccessException - db 관련 예외 발생 시
+     * @throws Exception - 그 외 예외 발생 시
+     *
+     * 이메일 인증코드 발송
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean sendEmailVerifyCode(EmailVO emailVO){
+        try {
+            String email = emailVO.getEmail();
+            String code = createRandomCode(); // 인증코드 생성
+            emailVO.setCode(code);
+            long createdAt = System.currentTimeMillis(); // 현재 시간(millis)
+            emailVO.setCreatedAt(createdAt);
+
+            boolean isSend = redisTemplate.hasKey("email:" + email); // 이메일 인증코드 발송여부 확인
+
+            if(isSend) { // 이미 인증코드가 발송된 경우
+                EmailVO verifyVO = (EmailVO) redisTemplate.opsForValue().get("email:" + email); // 발송된 인증코드
+                if(createdAt - verifyVO.getCreatedAt() > verifyTime) { // 인증코드 유효시간이 지났을 경우
+                    redisTemplate.delete("email:" + email); // 인증코드 삭제
+                }
+            }
+
+            // 이메일 발송
+            SimpleMailMessage message = new SimpleMailMessage();
+            message.setTo(email);
+            message.setSubject("이메일 인증 코드");
+            message.setText("인증 코드는 " + code + " 입니다.");
+            try {
+                mailSender.send(message);
+            } catch (Exception e) {
+                throw new CustomEmailSendFailException("이메일 발송에 실패했습니다.");
+            }
+
+            redisTemplate.opsForValue().set("email:" + email, emailVO, storeTime); // 인증코드 저장
+            return true;
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            redisTemplate.delete("email:" + emailVO.getEmail()); // 실패시 인증코드 삭제
+            throw e;
+        }
+    }
+
+    /**
+     * @param emailVO - 이메일 정보
+     * @return boolean - 이메일 인증코드 확인 결과
+     * @throws CustomEmailVerifyExpireException - 이메일 인증 만료 시
+     * @throws CustomEmailCodeNotMatchException - 이메일 인증코드 불일치 시
+     * @throws CustomEmailVerifyFailException - 이메일 인증 실패 시
+     * @throws DataAccessException - db 관련 예외 발생 시
+     * @throws Exception - 그 외 예외 발생 시
+     *
+     * 이메일 인증코드 확인
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean checkEmailVerifyCode(EmailVO emailVO){
+        try {
+            String email = emailVO.getEmail();
+            String code = emailVO.getCode();
+            long createdAt = System.currentTimeMillis(); // 현재 시간(millis)
+
+            boolean isSend = redisTemplate.hasKey("email:" + email); // 이메일 인증코드 발송여부 확인
+
+            if(isSend) { // 이미 인증코드가 발송된 경우
+                EmailVO verifyVO = (EmailVO) redisTemplate.opsForValue().get("email:" + email); // 발송된 인증코드
+                if(createdAt - verifyVO.getCreatedAt() > verifyTime) { // 인증코드 유효시간이 지났을 경우
+                    throw new CustomEmailVerifyExpireException("인증 시간이 만료되었습니다.");
+                }
+                String verifyCode = verifyVO.getCode(); // 발송된 인증코드
+                if(verifyCode.equals(code)) { // 인증코드가 일치하는 경우
+                    redisTemplate.delete("email:" + email); // 인증코드 삭제
+                } else { // 인증코드가 일치하지 않는 경우
+                    throw new CustomEmailCodeNotMatchException("인증코드가 일치하지 않습니다.");
+                }
+            } else { // 인증코드가 발송되지 않은 경우
+                throw new CustomEmailVerifyFailException("이메일 인증에 실패했습니다.");
+            }
+            return true;
+        } catch (DataAccessException dae) {
+            throw dae;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * @return String - 이메일 인증코드
+     *
+     * 이메일 인증코드 생성
+     */
+    private String createRandomCode() {
+        return String.valueOf((int)(Math.random() * 899999) + 100000); // 6자리 숫자
+    }
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/common/verify/vo/EmailVO.java (added)
+++ src/main/java/com/takensoft/common/verify/vo/EmailVO.java
@@ -0,0 +1,25 @@
+package com.takensoft.common.verify.vo;
+
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author  : 하석형
+ * @since   : 2025.05.20
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.20  |    하석형     | 최초 등록
+ *
+ * 이메일 관련 VO
+ */
+@Setter
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class EmailVO {
+    private String email;       // 이메일
+    private String code;        // 인증코드
+    private long createdAt;     // 인증코드 생성일시
+//    private LocalDateTime regDt;       // 등록일시
+}(파일 끝에 줄바꿈 문자 없음)
 
src/main/java/com/takensoft/common/verify/web/EmailController.java (added)
+++ src/main/java/com/takensoft/common/verify/web/EmailController.java
@@ -0,0 +1,66 @@
+package com.takensoft.common.verify.web;
+
+import com.takensoft.common.message.MessageCode;
+import com.takensoft.common.util.ResponseUtil;
+import com.takensoft.common.verify.service.EmailService;
+import com.takensoft.common.verify.vo.EmailVO;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author 하석형
+ * @since 2025.05.20
+ * @modification
+ *     since    |    author    | description
+ *  2025.05.20  |    하석형     | 최초 등록
+ *
+ * 이메일 관련 Controller
+ */
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping(value="/sys/email")
+public class EmailController {
+
+    private final EmailService emailService;
+    private final ResponseUtil resUtil;
+
+    /**
+     * @param emailVO - 이메일 정보
+     * @return ResponseEntity - 이메일 인증코드 발송 응답 결과
+     *
+     * 이메일 인증코드 발송
+     */
+    @PostMapping("/sendEmailVerifyCode.json")
+    public ResponseEntity<?> sendEmailVerifyCode(@RequestBody EmailVO emailVO) {
+
+        boolean result = emailService.sendEmailVerifyCode(emailVO);
+
+        return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
+    }
+
+    /**
+     * @param emailVO - 이메일 정보
+     * @return ResponseEntity - 이메일 인증코드 확인 응답 결과
+     *
+     * 이메일 인증코드 확인
+     */
+    @PostMapping("/checkEmailVerifyCode.json")
+    public ResponseEntity<?> checkEmailVerifyCode(@RequestBody EmailVO emailVO) {
+
+        boolean result = emailService.checkEmailVerifyCode(emailVO);
+
+        return resUtil.successRes(result, MessageCode.COMMON_SUCCESS);
+    }
+}
src/main/resources/application.yml
--- src/main/resources/application.yml
+++ src/main/resources/application.yml
@@ -48,6 +48,23 @@
       # 리눅스 환경
 #      static-locations: /home/cloud-user/uploadFiles
 
+  # 이메일 인증
+  mail:
+    host: smtp.gmail.com
+    port: 587
+    username: dhars3000@gmail.com
+    password: oejk enyp xowb mzsa
+    #    address: dhars3000@gmail.com
+    #    personal: TAKEN CMS SYSTEM
+    properties:
+      mail:
+        smtp: # SMTP 관련
+          auth: true
+          starttls: # 데이터 암호화 관련
+            enable: true
+    verifyTime: 600000 # 인증가능 시간: 10분
+    storeTime: 86400000 # 보관 시간: 24시간
+
 # Mybatis settings
 #mybatis:
 #  type-aliases-package: com.takensoft.**.**.vo, com.takensoft.**.**.dto, com.takensoft.common
src/main/resources/message/messages_ko.yml
--- src/main/resources/message/messages_ko.yml
+++ src/main/resources/message/messages_ko.yml
@@ -58,4 +58,12 @@
 
 # 파일 관련
 file:
-  upload_fail: "파일 업로드에 실패했습니다."
(파일 끝에 줄바꿈 문자 없음)
+  upload_fail: "파일 업로드에 실패했습니다."
+
+# 이메일 인증 관련
+email:
+  send_fail: "이메일 발송에 실패했습니다."
+  verify_success: "이메일 인증이 완료되었습니다."
+  verify_expired: "인증 시간이 만료되었습니다."
+  verify_fail: "이메일 인증에 실패했습니다."
+  code_not_match: "인증 코드가 일치하지 않습니다."
Add a comment
List