작업일지에 들어가며
이전 기능 구현에 이어 이번에는 사용자 정보를 입력받아 검증 후 비밀번호를 재설정하여
사용자 메일에 전송해주는 기능을 구현해보겠다.
🙏 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
'Project > Team' 카테고리의 다른 글
프로젝트 작업일지 : 로그인 (아이디 찾기) (0) | 2024.12.30 |
---|---|
프로젝트 작업일지 : 로그인 (아이디 기억하기) (2) | 2024.12.28 |
프로젝트 작업일지 : 로그인 (Spring Security - JWT - React) (0) | 2024.12.28 |
프로젝트 작업일지 : 로그인 (폼 만들기_React Hook Form) (2) | 2024.12.28 |
프로젝트 작업일지 : 회원가입 (이메일 중복 검사) (0) | 2024.12.27 |