08_Spring_240411(목)_62일차(1) - ★BoardProject★ - 4. 회원가입 - 이메일 유효성, 인증번호 받기

soowagger·2024년 4월 11일

8_Spring

목록 보기
14/38
post-thumbnail

4. 회원가입

✅ 회원가입 유효성 검사 여부 체크 객체

// **** 회원 가입 유효성 검사 ****

// 필수 입력 항목의 유효성 검사 여부를 체크하기 위한 객체
// - true == 해당 항목은 유효한 형식으로 작성됨
// - false == 해당 항목은 유효하지 않은 형식으로 작성됨
const checkObj = {
    "memberEmail" : false,
    "memberPw" : false,
    "memberPwConfirm" : false,
    "memberNickname" : false,
    "memberTel" : false,
    "authKey" : false,
    
}

4-1) 이메일

📌 이메일 유효성 검사

Member 컨트롤러

/** 회원 가입 페이지 이동
 * @return
 */
@GetMapping("signup")
public String signupPage() {
	return "member/signup";
}


@ResponseBody // 응답 본문(요청한 fetch)으로 돌려보냄
@GetMapping("checkEmail")
public int checkEmail(@RequestParam("memberEmail") String memberEmail) {
	return service.checkEmail(memberEmail);
}

signup.js

/* 이메일 유효성 검사 */

// 1) 이메일 유효성 검사에 사용될 요소 얻어오기
const memberEmail = document.querySelector("#memberEmail");
const emailMessage = document.querySelector("#emailMessage");


// 2) 이메일이 입력될 때 마다 유효성 검사 수행
memberEmail.addEventListener("input", e => {

    // 이메일 인증 후 이메일이 변경될 경우
    checkObj.authKey = false;
    document.querySelector("#authKeyMessage").innerText ="";

    // 작성된 이메일 값 얻어오기
    const inputEmail = e.target.value;

    // 3) 입력된 이메일이 없을 경우
    if(inputEmail.trim().length === 0) {
        emailMessage.innerText = "메일을 받을 수 있는 이메일을 입력해주세요.";

        // 메시지에 색상을 추가하는 클래스 모두 제거
        emailMessage.classList.remove('confirm', 'error');

        // 이메일 유효성 검사 여부를 false 변경
        checkObj.memberEmail = false;

        // 잘못 입력한 띄어쓰기가 있을 경우 없앰
        memberEmail.value = "";

        return;
    }

    // 4) 입력된 이메일이 있을 경우 정규식 검사
    //    (알맞은 형태로 작성했는지 검사)
    const regExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    
    // 입력 받은 이메일이 정규식과 일치하지 않는 경우
    // (알맞은 이메일 형태가 아닌 경우)
    if( !regExp.test(inputEmail) ) {
        emailMessage.innerText = "알맞은 이메일 형식으로 작성해주세요.";
        emailMessage.classList.add('error'); // 글자를 빨간색으로 변경
        emailMessage.classList.remove('confirm'); // 초록색 글자 제거
        checkObj.memberEmail = false; // 유효하지 않은 이메일임을 기록

        return;
    }
    
    // 5) 유효한 이메일 형식일 경우 중복 검사 수행
    // 비동기(ajax)
    fetch("/member/checkEmail?memberEmail=" + inputEmail)
    .then( resp => resp.text() )
    .then( count => {
        // count : 1이면 중복, 0이면 중복 아님

        if(count == 1) { // 중복 O
            emailMessage.innerText = "이미 사용중인 이메일입니다.";
            emailMessage.classList.add('error');
            emailMessage.classList.remove('confirm');
            checkObj.memberEmail = false; // 중복은 유효하지 않음
            return;
        }
        
        // 중복 X 경우
        emailMessage.innerText = "사용 가능한 이메일입니다.";
        emailMessage.classList.add('confirm');
        emailMessage.classList.remove('error');
        checkObj.memberEmail = true; // 유효한 이메일
    })
    .catch(error => {
        // fetch() 수행 중 예외 발생 시 처리
        console.log(error); // 발생한 예외 출력
    });

});

📌 인증번호 이메일로 받기

✋ 이메일 작업을 위한 기초 세팅

기재 생략 사항(보안 및 아직 미사용)

config.properties
EmailMapper, email-mapper.xml

EmailConfig

@Configuration
@PropertySource("classpath:/config.properties")
public class EmailConfig {
	
	// @Value : properties에 작성된 내용 중 키가 일치하는 값을 얻어와 필드에 대입
	@Value("${spring.mail.username}")
	private String userName;
	
	@Value("${spring.mail.password}")
	private String password;
	
	@Bean
	public JavaMailSender javaMailSender() {
		
		JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
		
		Properties prop = new Properties();
		prop.setProperty("mail.transport.protocol", "smtp"); // 전송 프로토콜
		prop.setProperty("mail.smtp.auth", "true"); // 인증 사용 여부
		prop.setProperty("mail.smtp.starttls.enable", "true"); // 안전한 연결 활성화 여부
		prop.setProperty("mail.debug", "true"); // 디버그 사용 여부
		prop.setProperty("mail.smtp.ssl.trust","smtp.gmail.com"); // 신뢰할 수 있는 SMTP 서버 호스트 지정
		prop.setProperty("mail.smtp.ssl.protocols","TLSv1.2"); // 버전 설정
		
		
		mailSender.setUsername(userName);
		mailSender.setPassword(password);
		mailSender.setHost("smtp.gmail.com"); // SMTP 서버 호스트 설정
		mailSender.setPort(587); // SMTP 서버 포트 설정
		mailSender.setDefaultEncoding("UTF-8"); // 기본 인코딩 설정
		mailSender.setJavaMailProperties(prop); // 위에 정의한 prop 세팅
		
		return mailSender;
		
	}
}

Email 컨트롤러

@Controller
@RequestMapping("email")
@RequiredArgsConstructor // final 필드 / @NotNull 필드에 자동으로 의존성 주입(@Autowired 생성자 방식 코드 자동완성)
public class EmailController {
	
	private final EmailService service;
	
	@ResponseBody
	@PostMapping("signup")
	public int signup(@RequestBody String email) {
		
		String authKey = service.sendEmail("signup", email);
		
	
		return 0;
	}
}


/* @Autowired를 이용한 의존성 주입 방법은 3가지 존재
 * 1) 필드
 * 2) Setter
 * 3) 생성자 (권장)
 * 
 * Lombok 라이브러리에서 제공하는
 * 
 * @RequiredArgsConstructor 를 이용하면
 * 
 * 필드 중
 * 1) 초기화 되지 않은 final이 붙은 필드
 * 2) 초기화 되지 않은 @NotNull이 붙은 필드
 * 
 * 1, 2에 해당하는 필드에 대한
 * @Autowired 생성자 구문을 자동 완성
 * 
 */

// 1) 필드에 의존성 주입(권장 X)
// @Autowired
// private EmailService service;


// 2) Setter 이용

// private EmailService service;

// @Autowired
// public void setService(EmailService service) {
// 	 this.service = service;
// }


// 3) 생성자
// private EmailService service;
// private MemberService service2;

// @Autowired
// public EmailController(EmailService service, MemberService service2) {
//	 this.service = service;
//	 this.service = service2;
// }

Email 서비스★

@Transactional // 예외 발생하면 롤백할게~ (기본값 커밋)
@Service
@RequiredArgsConstructor
public class EmailServiceImpl implements EmailService {
	
	// EmailConfig 설정이 적용된 객체(메일 보내기 기능)
	private final JavaMailSender mailSender;
	
	// Mapper 의존성 주입
	private final EmailMapper mapper;
	
	
	// 이메일 보내기
	@Override
	public String sendEmail(String htmlName, String email) {
		
		// 6자리 난수(인증 코드) 생성
		String authKey = createAuthKey();
		
		try {
			
			// 제목
			String subject = null;
			
			switch(htmlName) {
			case "signup" : 
				subject = "[boardProject] 회원가입 인증번호입니다."; break;				
			}
			
			// 인증 메일 보내기
			
			// MimeMessage : Java에서 메일을 보내는 객체
			MimeMessage mimeMessage = mailSender.createMimeMessage();
			
			// MimeMessageHelper : Spring에서 제공하는 메일 발송 도우미(간단 + 타임리프)
			MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
			
			// 1번 매개변수 : MimeMessage
			// 2번 매개변수 : 파일 전송 사용 ? true / false
			// 3번 매개변수 : 문자 인코딩 지정
			
			helper.setTo(email); // 받는 사람 이메일 지정
			helper.setSubject(subject); // 이메일 제목 지정
			
			helper.setText(authKey); // html보낼거임(추후 변경 예정)
			
			// CID(Content-ID)를 이용해 메일에 이미지 첨부
			// logo 추가 예정
			
			// 메일 보내기
			mailSender.send(mimeMessage);
			
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
		
		
		return null;
	}
	
	/** 인증번호 생성 (영어 대문자 + 소문자 + 숫자 6자리)
	    * @return authKey
	    */
   public String createAuthKey() {
   		String key = "";
   		for(int i=0 ; i< 6 ; i++) {
          
           int sel1 = (int)(Math.random() * 3); // 0:숫자 / 1,2:영어
          
           if(sel1 == 0) {
              
               int num = (int)(Math.random() * 10); // 0~9
               key += num;
              
           }else {
              
               char ch = (char)(Math.random() * 26 + 65); // A~Z
              
               int sel2 = (int)(Math.random() * 2); // 0:소문자 / 1:대문자
              
               if(sel2 == 0) {
                   ch = (char)(ch + ('a' - 'A')); // 대문자로 변경
               }
              
               key += ch;
           }
          
       }
       return key;
   }

}

/* Google SMTP를 이용한 이메일 전송하기
 * 
 * * SMTP(Simple Mail Transfer Protocol, 간단한 메일 전송 규약)
 * - 이메일 메시지를 보내고 받을 때 사용하는 약속(규약, 방법)
 * 
 * - Google SMTP 
 * 
 * Java Mail Sender -> Google SMTP -> 대상에게 이메일 전송
 * 
 * * Java Mail Sender에 Google SMTP 이용 설정 추가
 *  1) config.properties 내용 추가(계정, 앱비밀번호)
 *  2) EmailConfig.java
 */

signup.js

// 1. 변수 세팅

// 인증번호 받기 버튼
const sendAuthKeyBtn = document.querySelector("#sendAuthKeyBtn");

// 인증번호 입력 input
const authKey = document.querySelector("#authKey");

// 인증번호 입력 후 확인 버튼
const checkAuthKeyBtn = document.querySelector("#checkAuthKeyBtn");

// 인증번호 관련 메시지 출력 span
const authKeyMessage = document.querySelector("#authKeyMessage");

let authTimer; // 타이머 역할을 할 setInterval을 저장할 변수

const initMin = 4; // 타이머 초기값 (분)
const initSec = 59; // 타이머 초기값 (초)
const initTime = "05:00";

// 실제 줄어드는 시간을 저장할 변수
let min = initMin;
let sec = initSec;


// 2. 인증번호 받기 버튼 클릭 시 이벤트 생성

sendAuthKeyBtn.addEventListener("click", () => {
    
    // 재클릭에 대한 처리
    checkObj.authKey = false;
    authKeyMessage.innerText = "";

    // 중복되지 않은 유효한 이메일을 입력한 경우가 아니면
    if( !checkObj.memberEmail ) {
        alert("유효한 이메일 작성 후 클릭해 주세요.");
        return;
    }

    // 클릭시 타이머 숫자 초기화
    min = initMin;
    sec = initSec;

    // 이전 동작 중인 인터벌 클리어
    clearInterval(authTimer);

    // ***************************************************

    // 비동기로 서버에서 메일 보내기
    fetch("/email/signup", {
        method : "POST",
        headers : {"Content-Type" : "application/json"},
        body : memberEmail.value 
    })


    // ***************************************************  

    // 메일은 비동기로 서버에서 보내라고 하고
    // 화면에서는 타이머 시작하기
    authKeyMessage.innerText = initTime; // 05:00 세팅
    authKeyMessage.classList.remove('confirm', 'error'); // 검정 글씨
    
    alert("인증번호가 발송되었습니다.")

    // SetInterval(함수, 지연시간(ms))
    // - 지연시간(ms)만큼 시간이 지날 때 마다 함수 수행

    // clearInterval(Interval이 저장된 변수)
    // - 매개변수로 전달받은 interval을 멈춤

    // 인증 시간 출력(1초마다 동작)
    authTimer = setInterval( () => {
        
        authKeyMessage.innerText = `${addZero(min)}:${addZero(sec)}`;

        // 0분 0초인 경우 ("00:00" 출력 후)
        if(min == 0 && sec == 0) {
            checkObj.authKey = false; // 인증 못함
            clearInterval(authTimer); // 인터벌 멈춤
            authKeyMessage.classList.add('error');
            authKeyMessage.classList.remove('confirm');
            return;
        }

        // 0초인 경우(0초를 출력한 후)
        if(sec == 0) {
            sec = 60;
            min--;
        }

        sec--; // 1초 감소

    } , 1000); // 1초 지연시간

});


// 전달 받은 숫자가 10 미만인 경우(한자리) 앞에 0 붙여서 반환
function addZero(number) {
    if(number < 10) return "0" + number;
    else return number;
}

📌 메일에 HTML 입히기

이메일 보내기 메서드에 구문 수정 및 추가

helper.setText( loadHtml(authKey, htmlName), true ); // html보낼거임(추후 변경 예정)
// HTML 코드 해석 여부 true (innerHTML 해석)

// CID(Content-ID)를 이용해 메일에 이미지 첨부
// (파일첨부와는 다름, 이메일 내용 자체에 사용할 이미지 첨부)
// logo 추가 예정
helper.addInline("logo", new ClassPathResource("static/images/logo.jpg"));
// -> 로고 이미지를 메일 내용에 첨부하는데
//    사용하고 싶으면 "logo"라는 id를 작성해라

html → java 변환에 필요한 객체 templateEngine를 필드에 선언

// 타임리프(템플릿 엔진)을 이용해서 html 코드 -> java로 변환
private final SpringTemplateEngine templateEngine;

loadHtml 메서드 추가

// HTML 파일을 읽어와 String으로 변환(타임리프 적용)
private String loadHtml(String authKey, String htmlName) {
	
	// org.thymeleaf.context 선택
	Context context = new Context();
	
	// 타임리프가 적용된 HTML에서 사용할 값 추가
	context.setVariable("authKey", authKey);
	
	
	// templates/email 폴더에서 htmlName과 같은
	// ~.html 파일 내용을 읽어와 String으로 변환
	return templateEngine.process("email/" + htmlName, context);
}

signup.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!-- 메인은 header 불필요 -->
<body>
    <!-- css/js 파일은 이메일 첨부 불가 -->
    <div style="display: flex; flex-direction: column; align-items: center;">
        
        <!-- Content-ID로 등록된 이미지 중 logo를 출력 -->
        <img src="cid:logo" width="200px">

        <h3>
            아래 발급된 인증 번호를 제한 시간 내에
            <br>
            회원 가입 화면에 입력 후 인증 버튼을 눌러주세요.
        </h3>

        <h3 style="text-align: center; border: 3px solid black; 
            color: blue; width: 400px; padding: 10px;">
            <th:block th:text="${authKey}"></th:block>
        </h3>
    </div>
</body>
</html>

📌 인증번호 발송 결과 확인

서비스 > 이메일 보내기 메서드 구문 추가

// 이메일 + 인증번호를 "TB_AUTH_KEY" 테이블 저장
Map<String, String> map = new HashMap<>();

map.put("authKey", authKey);
map.put("email", email);

// 1) 해당 이메일이 DB에 존재하는 경우가 있을 수 있기 때문에
//    수정(update)을 먼저 수행
//    -> 1 반환 == 업데이트 성공 == 이미 존재해서 인증번호 변경
//    -> 0 반환 == 업데이트 실패 == 이메일 존재 X --> INSERT 시도

int result = mapper.updateAuthKey(map);

// 2) update 실패 시 insert 시도
if(result == 0) {
	result = mapper.insertAuthKey(map);
	
} 

// 수정, 삽입 후에도 result 가 0 == 실패
if(result == 0) return null;

// 성공
return authKey; // 오류 없이 완료되면 authKey 반환

email-mapper.xml

<!-- 인증 번호 수정 -->
<update id="updateAuthKey">
	UPDATE "TB_AUTH_KEY" SET
	"AUTH_KEY" = #{authKey},
	"CREATE_TIME" = SYSDATE
	WHERE "EMAIL" = #{email}
</update>

<!-- 인증 번호 삽입 -->
<insert id="insertAuthKey">
	INSERT INTO "TB_AUTH_KEY"
	VALUES(SEQ_KEY_NO.NEXTVAL, #{email}, #{authKey}, DEFAULT)
</insert>

컨트롤러에서 반환 값 전달

private final EmailService service;

@ResponseBody
@PostMapping("signup")
public int signup(@RequestBody String email) {
	
	String authKey = service.sendEmail("signup", email);
	
	if(authKey != null) { // 인증번호가 반환되서 돌아옴
						  // == 이메일 보내기 성공
		return 1;
	}
	
	// 이메일 보내기 실패
	return 0;
}

signup.js 인증번호 받기 버튼 클릭 이벤트

// ***************************************************

// 비동기로 서버에서 메일 보내기
fetch("/email/signup", {
    method : "POST",
    headers : {"Content-Type" : "application/json"},
    body : memberEmail.value 
})
.then( resp => resp.text())
.then( result => {
    if(result == 1) {
        console.log("인증 번호 발송 성공");
    } else {
        console.log("인증 번호 발송 실패");
    }
});

// ***************************************************  

📌 인증하기 버튼 결과 확인

💡 구글은 display: flex; 이후 스타일이 먹지 않음

✋ java - map 전달 / js - 객체 생성 포인트

// 입력받은 이메일, 인증번호로 객체 생성
const obj = {
    "email" : memberEmail.value,
    "authKey" : authKey.value
}

js

// 인증하기 버튼 클릭 시
// 입력된 인증번호를 비동기로 서버에 전달
// -> 입력된 인증번호와 발급된 인증번호가 같은지 비교
//    같으면 1, 아니면 0 반환
// 단, 타이머가 00:00초가 아닐 경우에만 수행

checkAuthKeyBtn.addEventListener("click", () => {
    
    if( min === 0 && sec === 0) { // 타이머가 00:00인 경우
        alert("인증번호 입력 제한시간을 초과하였습니다!");
        return;
    }

    if(authKey.value.length < 6) {
        alert("인증번호를 정확히 입력해 주세요.");
        return;
    }

    // 입력받은 이메일, 인증번호로 객체 생성
    const obj = {
        "email" : memberEmail.value,
        "authKey" : authKey.value
    }

    fetch("/email/checkAuthKey", {
        method : "POST",
        headers : {"Content-Type" : "application/json"},
        body : JSON.stringify(obj) // obj를 JSON으로 변경
    })
    .then(resp => resp.text())
    .then(result => {
        
        // == : 값 비교 "1" == 1 -> true
        // === : 값 + 타입비교 "1" == 1 -> false

        if(result == 0) {
            alert("인증번호가 일치하지 않습니다.");
            checkObj.authKey = false;
            return;
        }

        clearInterval(authTimer); // 타이머 멈춤
        
        authKeyMessage.innerText = "인증되었습니다.";
        authKeyMessage.classList.remove('error');
        authKeyMessage.classList.add('confirm');

        checkObj.authKey = true; // 인증 번호 검사여부 true

    });
});

컨트롤러

@ResponseBody
@PostMapping("checkAuthKey")
public int checkAuthKey(@RequestBody Map<String, Object> map) {
	
	// 입력 받은 이메일, 인증번호가 DB에 있는지 조회
	// 이메일 있고, 인증번호 일치 == 1
	// 아니면 0
	return service.checkAuthKey(map);
}

email-mapper.xml

<!-- 이메일, 인증번호 확인 -->
<select id="checkAuthKey" resultType="_int">
	SELECT COUNT(*)
	FROM "TB_AUTH_KEY"
	WHERE EMAIL = #{email}
	AND AUTH_KEY = #{authKey}
</select>

profile

0개의 댓글