프로젝트 작업일지 : 로그인 (비밀번호 찾기)

작업일지에 들어가며

 
이전 기능 구현에 이어 이번에는 사용자 정보를 입력받아 검증 후 비밀번호를 재설정하여
 
사용자 메일에 전송해주는 기능을 구현해보겠다.
 
🙏 Spring Boot + React 프로젝트 / 이메일 전송 기능 (비밀번호 찾기, 임시비밀번호 부여)
 
내가 원하는 기능을 아주 상세하게 작성해주셔서 해당 포스팅을 상당히 많은 참고를 하며 해봐야겠다.


구글 이메일 세팅

 
앱 비밀번호를 꼭 기억해두자!


백엔드 코드

1. application.properties

# GMail
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.properties.mail.deug=true
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.timeout=50000
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

spring.mail.username=메일계정
spring.mail.password=앱비밀번호

 

2. Controller

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

	private final AuthProcess authProcess;

	// 비밀번호 찾기
	@PostMapping("/find-password")
	public ResponseEntity<String> findPassword(@RequestBody Map<String, String> user) {
		// System.out.println(user.get("email"));
		// System.out.println(user.get("phone"));
		return authProcess.findPassword(user);
	}
}

 

3. Auth Service

AuthProcess

public interface AuthProcess {
	
	ResponseEntity<String> findPassword(Map<String, String> user);
}

 

AuthProcessImpl

@Service
@RequiredArgsConstructor
public class AuthProcessImpl implements AuthProcess {

	private final MembersRepository membersRepository;
	private final MailProcess mailProcess;

	// 비밀번호 재설정
	@Override
	public ResponseEntity<String> findPassword(Map<String, String> user) {
		Optional<Members> memberOptional = membersRepository.findOptionalByEmail(user.get("email"));

		if (memberOptional.isPresent()) {
			Members members = memberOptional.get();
			String phone = members.getPhone();
			String inputPhone = user.get("phone");

			if (phone.equals(inputPhone)) {
				// CompletableFuture를 사용하여 비동기적으로 메일 전송
				// 이렇게 안하면 메일 전송하고 완료된 후에 넘어가는데 3초의 딜레이 발생
				CompletableFuture.runAsync(() -> {
					try {
						mailProcess.sendEmailForCertification(user.get("email"));
					} catch (Exception e) {
						e.printStackTrace();
					}
				});
				return ResponseEntity.ok("메일 전송 성공");
			} else {
				return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("입력된 번호가 일치하지 않습니다."); // 휴대전화 불일치
			}
		} else {
			return ResponseEntity.status(HttpStatus.NOT_FOUND).body("해당 이메일로 등록된 사용자가 없습니다."); // 해당 이메일으로 등록된 계정이 없을 경우
		}
	}
}

 

4. Mail Service

@Service
@RequiredArgsConstructor
public class MailProcess {
	
	private final JavaMailSender javaMailSender;
	private final CertificationGenerator certificationGenerator;
	private final MembersRepository membersRepository;
	private final PasswordEncoder passwordEncoder;
	
	public void sendEmailForCertification(String email) throws NoSuchAlgorithmException, MessagingException {
		
		// 비밀번호 생성
		String certificationPassword = certificationGenerator.createCertificationPassword();

		System.out.println("Certification Password: " + certificationPassword);

		String content = String.format("<br> 임시비밀번호: %s <br><br><br> 로그인 후 마이페이지에서 비밀번호를 수정해주세요.",
	            certificationPassword
	           );

        // 비밀번호 해싱
        String password = passwordEncoder.encode(certificationPassword);
        System.out.println("Hashed password to save: " + password);
        
        // DB에 비밀번호 저장
        Optional<Members> optionalMember = membersRepository.findOptionalByEmail(email);
        if (optionalMember.isPresent()) {
        	Members member = optionalMember.get();
        	member.setPassword(password); // 비밀번호 설정
        	membersRepository.save(member); // 변경된 비밀번호를 DB 저장
        } else {
            System.out.println("No member found with email: " + email); // 이메일로 회원을 찾을 수 없을 때
        }

        // 이메일 전송
        sendPasswordMail(email, content);
        System.out.println("Email sent to: " + email); // 이메일 전송 여부 확인
    }

    private void sendPasswordMail(String email, String content) throws MessagingException {

        // 이메일 객체 생성
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);

        // 수신자, 제목, 내용 설정
        helper.setTo(email);
        helper.setSubject("PROJECT 비밀번호 변경 메일");
        helper.setText(content, true); // html변환 전달

        // 메일 전송
        javaMailSender.send(mimeMessage);
	}
}

 
 

5. CertificationGenerator ( 난수 생성 클래스 )

@Component
public class CertificationGenerator {

	// 영문 알파벳
    private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    public String createCertificationPassword() throws NoSuchAlgorithmException {
        StringBuilder result = new StringBuilder();

        // 영어 알파벳과 숫자를 조합하여 난수 생성
        do {
            // 10자리의 난수 생성
            for (int i = 0; i < 10; i++) {
                // SecureRandom을 이용하여 0부터 35 사이의 난수 생성
                int randomIndex = SecureRandom.getInstanceStrong().nextInt(ALPHABET.length() + 10);

                // 숫자인 경우
                if (randomIndex < 10) {
                    result.append(randomIndex);
                }
                // 알파벳인 경우
                else {
                    // ALPHABET 문자열에서 랜덤한 영문 알파벳 선택하여 결과 추가
                    result.append(ALPHABET.charAt(randomIndex - 10));
                }
            }
        } while (result.length() != 10);

        return result.toString();
    }
}

프론트엔드 코드

이전의 이메일 찾기 모달을 다시 활용해주자! 사용자 이메일과 휴대전화 번호를 받으면 되겠다.

비밀번호 찾기 모달

const FindPassword = () => {
    const {
      register,
      handleSubmit,
      formState: { errors },
      reset,
    } = useForm();
  
    const [isModalOpen, setModalOpen] = useState(false);
    const [isLoading, setLoading] = useState(false);
    const [result, setResult] = useState(null);
  
    const handleFindPassword = async (data) => {
      setLoading(true);
  
      try {
        const response = await axios.post(
          "http://localhost:8080/auth/find-password",
          data
        );
        setResult({
          status: "success",
          title: "인증 성공",
          message: "인증 성공 : 이메일을 확인해주세요.",
        });
      } catch (error) {
        setResult({
          status: "error",
          title: "정보 불일치",
          message: "정보 불일치 : 입력하신 정보를 확인해주세요.",
        });
      } finally {
        setLoading(false);
      }
    };

    const handleCloseModal = () => {
      setModalOpen(false);
      reset();
      setResult(null);
    };
  
    const modalStyles = {
      modal: {
        position: "fixed",
        top: 0,
        left: 0,
        width: "100%",
        height: "100%",
        background: "rgba(0, 0, 0, 0.5)",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        zIndex: 1000,
      },
      modalContent: {
        background: "white",
        padding: "40px",
        borderRadius: "12px",
        boxShadow: "0 4px 15px rgba(0, 0, 0, 0.2)",
        animation: "fadeIn 0.3s ease",
        width: "700px",
        maxWidth: "100%",
        height: "auto",
        textAlign: "left",
        position: "relative",
        top: "-50px",
        fontFamily: "'Arial', sans-serif",
      },
      button: {
        width: "auto",
        padding: "10px 20px",
        height: "40px",
        borderRadius: "8px",
        backgroundColor: "#ff6f00",
        color: "white",
        fontSize: "1em",
        border: "none",
        cursor: "pointer",
        marginBottom: "10px",
        transition: "background-color 0.3s ease",
      },
      buttonDisabled: {
        backgroundColor: "#ffcc80",
        cursor: "not-allowed",
      },
      cancelButton: {
        width: "auto",
        padding: "10px 20px",
        height: "40px",
        borderRadius: "8px",
        border: "2px solid lightgray",
        backgroundColor: "white",
        color: "black",
        fontSize: "1em",
        cursor: "pointer",
        transition: "background-color 0.3s ease",
      },
      errorMessage: {
        color: "red",
        fontSize: "0.9em",
        marginTop: "-15px",
      },
      modalTitle: {
        marginBottom: "20px",
        fontSize: "1.5em",
      },
      inputField: {
        width: "100%",
        padding: "12px",
        borderRadius: "8px",
        border: "1px solid #ccc",
        marginBottom: "15px",
        fontSize: "1em",
      },
      buttonContainer: {
        display: "flex",
        justifyContent: "flex-end",
        gap: "10px",
        width: "100%",
      },
      resultMessage: {
        color: result?.status === "error" ? "red" : "green",
        fontSize: "1em",
        marginLeft: "15px",
        display: "flex",
        alignItems: "center",
      },
    };
  
    return (
      <>
        <button type="button" onClick={() => setModalOpen(true)} style={{ fontSize: "0.9em" }}>
          비밀번호 찾기
        </button>
  
        {isModalOpen && (
          <div style={modalStyles.modal} onClick={() => setModalOpen(false)}>
            <div
              style={modalStyles.modalContent}
              onClick={(e) => e.stopPropagation()}
            >
              <h2 style={modalStyles.modalTitle}>비밀번호 찾기</h2>
              <div>
                <label>이메일</label>
                <input
                  type="email"
                  placeholder="가입하신 계정의 이메일을 입력해주세요."
                  {...register("email", {
                    required: "이메일은 필수 입력입니다.",
                    pattern: {
                      value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
                      message: "올바른 이메일 형식으로 입력해주세요.",
                    },
                  })}
                  style={modalStyles.inputField}
                />
                {errors.email && (
                  <p style={modalStyles.errorMessage}>{errors.email.message}</p>
                )}
              </div>
              <div>
                <label>휴대전화</label>
                <input
                  type="text"
                  placeholder="'-'를 제외한 숫자 11자리를 입력해주세요."
                  {...register("phone", {
                    required: "휴대전화는 필수 입력입니다.",
                    pattern: {
                      value: /^[0-9]{11}$/,
                      message: "'-'를 제외한 숫자 11자리를 입력해주세요.",
                    },
                  })}
                  style={modalStyles.inputField}
                />
                {errors.phone && (
                  <p style={modalStyles.errorMessage}>{errors.phone.message}</p>
                )}
              </div>
              <div style={modalStyles.buttonContainer}>
                {result && (
                  <div style={modalStyles.resultMessage}>{result.message}</div>
                )}
                <button
                  type="button"
                  style={isLoading ? modalStyles.buttonDisabled : modalStyles.button}
                  disabled={isLoading}
                  onClick={handleSubmit(handleFindPassword)}
                >
                  {isLoading ? "처리 중..." : "비밀번호 찾기"}
                </button>
                <button
                  type="button"
                  onClick={handleCloseModal}
                  style={modalStyles.cancelButton}
                >
                  나가기
                </button>
              </div>
            </div>
          </div>
        )}
      </>
    );
  };
  
  export default FindPassword;

비밀번호 찾기 테스트


비밀번호 재설정 후 메일 전송 확인

입력 정보 불일치 시 검증
 

바뀐 비밀번호로 로그인 성공!


작업일지를 마치며

✨ 나의 생각

참고했던 블로그가 너무 너무 상세하게 잘 작성해주셔서 큰 에러 없이 비밀번호 찾기 기능을 구현했다.

단지 따라만한 정도라 조금 짬을 내서 코드를 상세히 분석해서 이해해봐야겠다!

이 기능 구현을 마지막으로 로그인 페이지에서 관련된 기능들을 모두 구현해주었다.

우선 나의 1차 개발 범위까지 마무리되어 다시 팀 회의를 통해 2차 개발 기간동안에

어떤 기능을 만들지 논의 해봐야겠다.

Reference

 
🙏 Spring Boot + React 프로젝트 / 이메일 전송 기능 (비밀번호 찾기, 임시비밀번호 부여)