TOP
본문 바로가기
[파이썬 Projects]/<파이썬 웹개발>

[파이썬] FastAPI - 메모 앱 프로젝트 15: 이메일 기능 보완

by 기록자_Recordian 2025. 5. 14.
728x90
반응형
이전 내용
 

[파이썬] FastAPI - 메모 앱 프로젝트 14: 비밀번호 찾기(수정)

이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 13: 아이디 찾기이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 12: 파일 분할하기이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 11: 회원 탈퇴이전 내용 [

puppy-foot-it.tistory.com


이메일 발송 기능 보완
비밀번호 변경 시 이메일 안내 기능 구현 등

 

혹시나, 계정 정보가 노출되어 타인이 내 계정의 비밀번호를 변경했을 경우 사용자에게 이메일로 안내하여 비밀번호를 다시 변경할 수 있도록 하는 기능을 추가해 준다.

또한, 모든 이메일 전송 내용에 페이지 링크를 추가하여 이메일에서 바로 페이지로 접속할 수 있도록 보완해 본다.

 

◆ email_class.py

email_class.py 파일에 비밀번호 변경 시 email을 발송하는 클래스를 생성해 준다.

class EmailServiceSendNewPW:
    def send_email(
        self,
        receiver_email: str,
        username: str,
    ):
        sender_email = os.getenv('EMAIL_ADDRESS')
        password = os.getenv('EMAIL_PASSWORD')

        message = MIMEMultipart("alternative")  # <-- 중요! HTML을 포함하려면 "alternative"로 설정
        message["From"] = sender_email
        message["To"] = receiver_email
        message["Subject"] = "비밀번호 변경 안내 이메일"

        # HTML 본문 구성
        html_body = f"""
        <html>
            <body>
                <p><strong>{username}</strong> 사용자님의 비밀번호가 변경되었습니다.</p>
                <p>만약 본인이 변경한 것이 아니라면 즉시 아래 링크를 통해 비밀번호를 변경해 주세요.</p>
                <p><a href="http://localhost:8000/change_pw" target="_blank">➡️ 비밀번호 변경하러 가기</a></p>
            </body>
        </html>
        """

        message.attach(MIMEText(html_body, "html"))  # HTML 형식으로 첨부

        try:
            with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
                server.login(sender_email, password)
                server.send_message(message)
            print(f"이메일이 {receiver_email}로 성공적으로 전송되었습니다.")
        except Exception as e:
            print(f"이메일 전송 실패: {e}")

★ 이메일 내용에 링크 넣기

이메일 본문에서 클릭 가능한 링크를 만들고 싶다면, 단순한 plain text 형식이 아니라 HTML 형식으로 이메일을 보내야 한다. 따라서, "plain" 대신 "html" 형식으로 설정하고, 링크를 <a> 태그로 작성하면 된다.

항목 설명
target="_blank" 새 창에서 열기
text/html HTML 형식으로 이메일 보내기
MIMEMultipart("alternative") plain + html 모두 허용 (스팸 차단 줄이기)

 

 

◆ email.service.py

이번엔 email.service.py 파일에 비밀번호 변경 시 이메일을 발송하는 함수를 정의해 준다.

# EmailServiceSendNewPW 클래스 추가
from service.email_class import 기존 이메일 클래스들, EmailServiceSendNewPW

# 비밀번호 변경 안내 이메일 전송
def send_changed_pw_email(email: str, username: str):
    # 이메일 전송
    email_service = EmailServiceSendNewPW() # EmailServiceSendNewPW 클래스 인스턴스 생성
    try:
        email_service.send_email(receiver_email=email, username=username) # 이메일 전송
        logger.info(f"비밀번호 변경 안내 이메일을 {email}로 전송했습니다.")
    except Exception as e:
        # 이메일 전송 실패 시 에러 메시지 반환
        logger.error(f"이메일 전송 실패: {e}") # 에러 로깅

 

◆ users_controller.py

  • email.service.py 의 비밀번호 변경 코드도 수정해 준다.
  • 먼저 service 디렉터리의 email_service.py 내에  앞서 생성한 send_cahnged_pw_email 함수를 import 해준다.
  • 비밀번호 업데이트 처리하는 함수인 update_user_password() 함수와 return 사이에 send_cahnged_pw_email 함수를 추가해 주고, 예외 처리와 로깅 부분을 추가해 준다.
  • 함수의 인자 중 하나인 email의 경우, UserUpdate 클래스에 추가하게 되면 수정해야 할 로직들이 많아지므로, user db에서 불러오도록 한다.
# send_changed_pw_email 함수 추가
from service.email_service import 기존 함수들, send_changed_pw_email

# 비밀번호 변경
@router.put("/change_pw")
async def update_password(req: UserUpdate, db: Session = Depends(get_db)):
    username = req.username
    current_password = req.current_password  # 현재 비밀번호
    new_password = req.new_password  # 새로운 비밀번호
    new_password_confirm = req.new_password_confirm  # 새로운 비밀번호 확인

.....

    # 비밀번호 업데이트 처리
    update_user_password(db, user, new_password)  # 기존 함수를 호출하여 비밀번호 업데이트

    # 비밀번호 변경 안내 이메일 발송
    try:
        send_changed_pw_email(email=user.email, username=username)
        logger.info(f"비밀번호 변경 안내 이메일을 {user.email}로 성공적으로 전송했습니다.")
    except Exception as e:
        logger.error(f"이메일 전송 실패: {e}")
        raise HTTPException(status_code=500, detail="이메일 전송에 실패했습니다. 다시 시도해 주세요")

    return {"success": True, "message": "비밀번호가 성공적으로 변경되었습니다."}

비밀번호 변경 이메일 테스트

 

main.py를 실행하여 서버를 실행한 뒤, 비밀번호를 변경해 본다.

 

확인 이메일이 잘 발송되고, 이메일 내의 링크를 클릭하면 비밀번호 변경하는 사이트로 이동된다.

 


email_class.py 파일 내
다른 클래스들 보완

 

테스트가 성공한 것을 확인했으니, email_class.py 파일 내의 다른 클래스들도 이메일 발송 내용을 링크를 포함한 내용으로 바꿔주도록 한다.

 

◆ 회원 가입 환영 이메일 클래스

회원 가입 시 전송되는 이메일에 홈페이지 링크를 추가하여 로그인을 할 수 있도록 수정해 주었다.

# 회원가입 환영 이메일
class EmailServiceWelcome:
    def send_email(
            self,
            receiver_email: str,
    ):
        sender_email = os.getenv('EMAIL_ADDRESS')
        password = os.getenv('EMAIL_PASSWORD')

        message = MIMEMultipart("alternative")
        message["From"] = sender_email
        message["To"] = receiver_email
        message["Subject"] = "회원 가입을 환영합니다."

        # HTML 본문 구성
        html_body = f"""
        <html>
            <body>
                <p><strong>메모 앱</strong> 회원가입을 환영합니다!</p>
                <p>앞으로 많은 이용 부탁 드리며, 사용에 불편함이 있거나, 제안사항이 있으시면</p>
                <p>언제든 피드백 부탁 드립니다! 👍🏻</p>
                <p><a href="http://localhost:8000" target="_blank">✅ 로그인하러 가기</a></p>
            </body>
        </html>
        """

        message.attach(MIMEText(html_body, "html"))
        try:
            with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
                server.login(sender_email, password)
                server.send_message(message)
            logger.info(f"회원가입 이메일이 {receiver_email}로 성공적으로 전송되었습니다.")
        except Exception as e:
            logger.error(f"이메일 전송 실패: {e}")

 

◆ 회원 탈퇴 이메일 클래스

회원 탈퇴 시 전송되는 이메일에 홈페이지 링크를 추가하여 로그인을 할 수 있도록 수정해 주었다.

# 회원 탈퇴 이메일
class EmailServiceBye:
    def send_email(
            self,
            receiver_email: str,
    ):
        sender_email = os.getenv('EMAIL_ADDRESS')
        password = os.getenv('EMAIL_PASSWORD')

        message = MIMEMultipart("alternative")
        message["From"] = sender_email
        message["To"] = receiver_email
        message["Subject"] = "탈퇴 완료 이메일."

        # HTML 본문 구성
        html_body = f"""
        <html>
            <body>
                <p><strong>메모 앱</strong> 회원 탈퇴가 완료되었습니다.</p>
                <p>그동안 고객님의 이용에 감사 드리며, 더 나은 서비스가 되도록 노력하겠습니다.🙇🏻‍♀️</p>
                <p>곧 고객님을 다시 만날 날을 기다리겠습니다. 🙋🏻‍♂️</p>
                <p><a href="http://localhost:8000" target="_blank">✅ 회원가입하러 가기</a></p>
            </body>
        </html>
        """

        message.attach(MIMEText(html_body, "html"))

        try:
            with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
                server.login(sender_email, password)
                server.send_message(message)
            logger.info(f"탈퇴 안내 이메일이 {receiver_email}로 성공적으로 전송되었습니다.")
        except Exception as e:
            logger.error(f"이메일 전송 실패: {e}")

 

◆ 아이디 찾기 이메일 클래스

이메일 찾기 요청 시 전송되는 이메일에 홈페이지 링크를 추가하여 로그인을 할 수 있도록 수정해 주었다.

# 아이디 찾기 이메일
class EmailServiceFindId:
    def send_email(
            self,
            receiver_email: str,
            username: str,
    ):
        sender_email = os.getenv('EMAIL_ADDRESS')
        password = os.getenv('EMAIL_PASSWORD')

        message = MIMEMultipart("alternative")
        message["From"] = sender_email
        message["To"] = receiver_email
        message["Subject"] = "아이디 찾기 이메일."

        # HTML 본문 구성
        html_body = f"""
        <html>
            <body>
                <p>사용자님의 아이디는 <strong>{username}</strong> 입니다.</p>
                <p><a href="http://localhost:8000" target="_blank">✅ 로그인하러 가기</a></p>
            </body>
        </html>
        """

        message.attach(MIMEText(html_body, "html"))

        try:
            with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
                server.login(sender_email, password)
                server.send_message(message)
            logger.info(f"아이디가 {receiver_email}로 성공적으로 전송되었습니다.")
        except Exception as e:
            logger.error(f"이메일 전송 실패: {e}")

 

◆ 임시비밀번호 안내 이메일 클래스

임시비밀번호 생성 요청 시 전송되는 이메일에 비밀번호를 변경하는 링크를 추가하여 비밀번호를 변경할 수 있도록 수정해 주었다.

# 임시 비밀번호 이메일
class EmailServiceSendTempPW:
    def send_email(
        self,
        receiver_email: str,
        username: str,
        temp_password: str,
    ):
        sender_email = os.getenv('EMAIL_ADDRESS')
        password = os.getenv('EMAIL_PASSWORD')

        message = MIMEMultipart("alternative")
        message["From"] = sender_email
        message["To"] = receiver_email
        message["Subject"] = "임시 비밀번호 이메일"

         # HTML 본문 구성
        html_body = f"""
        <html>
            <body>
                <p><strong>{username}</strong>님의 임시비밀번호는 {temp_password} 입니다.</p>
                <p>해당 비밀번호는 임시 비밀번호이므로, 비밀번호를 변경하는 것을 권장 드립니다.</p>
                <p>아래 링크를 통해 비밀번호를 변경해 주세요.</p>
                <p><a href="http://localhost:8000/change_pw" target="_blank">➡️ 비밀번호 변경하러 가기</a></p>
            </body>
        </html>
        """

        message.attach(MIMEText(html_body, "html"))

        try:
            with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
                server.login(sender_email, password)
                server.send_message(message)
            logger.info(f"임시 비밀번호가 {receiver_email}로 성공적으로 전송되었습니다.")
        except Exception as e:
            logger.error(f"이메일 전송 실패: {e}")

 


테스트 하기

 

main.py를 실행하여 서버를 실행한 뒤, 이메일 내용을 확인하고, 지정한 링크로 제대로 이동하는지 테스트해본다.

◆ 아이디 찾기

 

◆  회원 탈퇴

 

◆  회원 가입

 

◆  임시 비밀번호

 

잘 구현된다.


다음 내용

 

[파이썬] FastAPI - 메모 앱 프로젝트 16: 메인 페이지 나누기

이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 15: 이메일 기능 보완이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 14: 비밀번호 찾기(수정)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 13: 아이디 찾

puppy-foot-it.tistory.com

728x90
반응형