SW 프로젝트 12주차 활동 일지

고태경·2023년 11월 14일
0

SW 프로젝트

목록 보기
6/9
post-thumbnail

1주차 OT 8/30
2주차 주제 선정 (타임캡슐) 9/6
3주차 주제 변경 (냉장고 관리 웹사이트) 9/13
4주차 주제 결정 및 plan A, B 설정 9/20
5주차 프로젝트 첫 번째 중간 보고 9/27 (9/21~9/26)
6주차 프로젝트 두 번째 중간 보고 9/27 (9/27~10/3)
7주차 프로젝트 계획 보고서 발표
8주차 시험 주 (자습)
9주차 프로젝트 세 번째 중간 보고
10주차 프로젝트 네 번째 중간 보고(~10/31)
11주차 프로젝트 다섯 번째 중간 보고(~11/07)

12주차 중간점검 (시연)

이번 주차에 한 것

1. 자동 메일 전송 코드 작성

https://bill1224.tistory.com/256 참고

import smtplib
from email.mime.text import MIMEText

# 이메일 서버 연결 정보
smtp_server = 'smtp.gmail.com'
smtp_port = 587
smtp_username = '보내는 메일
smtp_password = '비밀번호'

# 이메일 메세지 설정
subject = 'Hello, World!'
body = 'This is a test email.'
sender = '보내는 메일'
receiver = '받는 메일'

# 이메일 메세지 생성
msg = MIMEText(body)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = receiver

# 이메일 발송
with smtplib.SMTP(smtp_server, smtp_port) as server:
    server.starttls()
    server.login(smtp_username, smtp_password)
    server.send_message(msg)

2. 마감일에 따른 메일 전송 로직 고민

  1. 자동메일전송 파일을 함수화
  2. 입력폼에서 내용 저장
  3. 스케줄러 발현 (입력되어 있는 start 날짜 3일 전에 실헹)
  4. 자동메일전송 함수 실행

3. 스케줄러 구현

pip install apscheduler

# 이메일 전송 함수
def send_email_function(email, name, date):
   # 이메일 서버 연결 정보
   smtp_server = 'smtp.gmail.com'  # SMTP 서버 주소 (Gmail의 경우)
   smtp_port = 587  # SMTP 서버 포트 (Gmail의 TLS 포트), 465와의 차이는?
   smtp_username = 'sw.project.django@gmail.com'  # 발신자 Gmail 계정
   smtp_password = 'brdo ybhi dopd wibz'  # 발신자 Gmail 비밀번호


   # 이메일 메세지 설정
   subject = f' 장고 : {name}의 소비기한이 3일 남았습니다!'  # 이메일 제목
   body = f'{name}의 소비기한이 3일 남았습니다!  +++ [웹크롤링 음식명 내용]'  # 이메일 본문 내용
   sender = 'sw.project.django@gmail.com'  # 발신자 이메일 주소
   # receiver = 'taegeong@naver.com'  # 수신자 이메일 주소

   # 이메일 메세지 생성
   msg = MIMEText(body)  # 이메일 본문을 MIMEText 객체로 생성
   msg['Subject'] = subject  # 이메일 제목 설정
   msg['From'] = sender  # 발신자 이메일 주소 설정
   msg['To'] = email  # 수신자 이메일 주소 설정

   # 이메일 발송
   with smtplib.SMTP(smtp_server, smtp_port) as server:  # SMTP 서버 연결
       server.starttls()  # TLS(전송 계층 보안) 시작
       server.login(smtp_username, smtp_password)  # 이메일 계정 로그인
       server.send_message(msg)  # 메시지 발송


# 이메일 스케줄링 함수
def schedule_email(user_email, title, event_date):
   # 날짜 3일 전 계산
   # send_date = datetime.strptime(event_date, "%Y-%m-%d") - timedelta(days=3)
   # 'YYYY-MM-DD HH:MM:SS' 형식으로 날짜와 시간 모두를 해석
   send_date = datetime.strptime(event_date, "%Y-%m-%d %H:%M:%S") - timedelta(days=3)

   timezone = pytz.timezone('Asia/Seoul')

   scheduler = BackgroundScheduler()
   scheduler.add_job(
       send_email_function,
       'date',
       run_date=send_date.replace(tzinfo=timezone),
       args=[user_email, title, event_date]
   )
   scheduler.start()
# 이메일 전송 함수
def send_email_function(email, name, date):
    # 이메일 서버 연결 정보
    smtp_server = 'smtp.gmail.com'  # SMTP 서버 주소 (Gmail의 경우)
    smtp_port = 587  # SMTP 서버 포트 (Gmail의 TLS 포트), 465와의 차이는?


    # 이메일 메세지 설정
    subject = f' 장고 : {name}의 소비기한이 3일 남았습니다!'  # 이메일 제목
    body = f'{name}의 소비기한이 3일 남았습니다!  +++ [웹크롤링 음식명 내용]'  # 이메일 본문 내용
    sender = 'sw.project.django@gmail.com'  # 발신자 이메일 주소
    # receiver = 'taegeong@naver.com'  # 수신자 이메일 주소

    # 이메일 메세지 생성
    msg = MIMEText(body)  # 이메일 본문을 MIMEText 객체로 생성
    msg['Subject'] = subject  # 이메일 제목 설정
    msg['From'] = sender  # 발신자 이메일 주소 설정
    msg['To'] = email  # 수신자 이메일 주소 설정

    # 이메일 발송
    with smtplib.SMTP(smtp_server, smtp_port) as server:  # SMTP 서버 연결
        server.starttls()  # TLS(전송 계층 보안) 시작
        server.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD)  # 이메일 계정 로그인
        server.send_message(msg)  # 메시지 발송

3. 보안 문제 (settings.py에 비밀번호가 그대로 유출됨)

이런 메일을 받았다.

cd django_calender_prj
git rm --cached settings.py
git commit -m "Remove settings.py from tracking"
git push

4. TDD 방식으로 테스트 하기 (스케줄링 및 자동메일전송)

send_email_function의 기능을 확인하는 테스트
실제 이메일을 보내지 않으면서 함수의 로직이 올바르게 작동하는지 검증하는 접근이 필요
이를 위해 Python의 표준 라이브러리인 unittest.mock을 사용할 수 있음
이 방법으로 실제 이메일 서버에 연결하지 않고 함수가 호출되었는지, 예상대로 매개변수가 전달되었는지 등을 확인할 수 있음

from django.test import TestCase
from calender.views import send_email_function
from unittest.mock import patch, MagicMock

class EmailTest(TestCase):
    @patch('calender.views.smtplib.SMTP')
    def test_send_email_function(self, mock_smtp):
        # Mock 객체 설정
        mock_server = MagicMock()
        mock_smtp.return_value.__enter__.return_value = mock_server

        # 함수 호출
        email = 'test@example.com'
        name = '테스트 재료'
        date = '2023-11-20'
        send_email_function(email, name, date)

        # 함수가 호출되었는지 검증
        mock_server.send_message.assert_called()

        # send_message가 호출된 인자 검증
        args, kwargs = mock_server.send_message.call_args
        sent_message = args[0]
        self.assertIn(email, sent_message['To'])
        self.assertIn(name, sent_message.get_payload())
        self.assertIn(date, sent_message.get_payload())

이 코드는 send_email_function이 smtplib.SMTP 클래스를 사용하여 이메일을 보내는 로직을 모의(Mock) 객체를 사용해 테스트
@patch 데코레이터를 사용하여 실제로 SMTP 서버에 연결하는 대신 모의 객체를 사용하고, 함수가 호출될 때 예상한 대로 작동하는지 확인

python manage.py test calender.tests.EmailTest.test_send_email_function

보안 주의사항:
이메일 주소와 비밀번호는 민감한 정보입니다. 코드나 GitHub과 같은 공개된 곳에 이러한 정보를 직접 작성하는 것은 보안상 위험할 수 있습니다.
보안을 강화하기 위해 환경 변수나 Django의 secrets 모듈을 사용하여 이메일 계정 정보를 관리하는 것이 좋습니다.
이메일을 보내기 위해 사용하는 계정은 '앱 비밀번호'나 '보안 수준이 낮은 앱의 액세스'를 활성화해야 할 수도 있습니다. (특히 Gmail의 경우)

이번 주차 이슈

1. 메일 전송을 위한 구글 로그인 실패

해결

시도1
'보안 수준이 낮은 앱에서 계정에 액세스하도록 허용'을 하려했음
but ...

계정을 안전하게 보호하기 위해 2022년 5월 30일부터 ​​Google은 사용자 이름과 비밀번호만 사용하여 
Google 계정에 로그인하도록 요청하는 서드 파티 앱 또는 기기의 사용을 더 이상 지원하지 않습니다.

?!?!!?!?!

https://www.infoking.site/138

시도2 (해결)
앱 비밀번호 생성 후 진행
https://bill1224.tistory.com/256

2. 이벤트 등록시 등록이 안되고 오류 발생

datetime.strptime 함수가 event_date 문자열을 해석하는 데 실패
오류 메시지 "unconverted data remains: 00:00:00"는 주어진 형식("%Y-%m-%d")이 문자열의 전체를 해석하지 못했다는 것을 나타냄. 이 경우, 문자열에 시간 정보(00:00:00)가 포함되어 있지만, 지정된 형식에는 날짜만 포함되어 있기 때문에 문제가 발생

(venv) PS C:\django_calender\django_calender> python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
November 15, 2023 - 13:34:38
Django version 4.2.6, using settings 'django_calender_prj.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

[15/Nov/2023 13:34:46] "GET /add_event?title=20%EC%9D%BC&start=2023-11-20%2000%3A00%3A00&end=2023-11-21%2000%3A00%3A00&f_category=food HTTP/1.1" 301 0
Internal Server Error: /add_event/
Traceback (most recent call last):
  File "C:\django_calender\venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\contrib\auth\decorators.py", line 23, in _wrapper_view
    return view_func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\django_calender\calender\views.py", line 112, in add_event
    schedule_email(user_email, title, event_date)
  File "C:\django_calender\django_calender\calender\views.py", line 76, in schedule_email
    send_date = datetime.strptime(event_date, "%Y-%m-%d") - timedelta(days=3)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\migyu\AppData\Local\Programs\Python\Python311\Lib\_strptime.py", line 568, in _strptime_datetime
    tt, fraction, gmtoff_fraction = _strptime(data_string, format)
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\migyu\AppData\Local\Programs\Python\Python311\Lib\_strptime.py", line 352, in _strptime
    raise ValueError("unconverted data remains: %s" %
ValueError: unconverted data remains:  00:00:00
[15/Nov/2023 13:34:46] "GET /add_event/?title=20%EC%9D%BC&start=2023-11-20%2000%3A00%3A00&end=2023-11-21%2000%3A00%3A00&f_category=food HTTP/1.1" 500 87439

해결 : strptime 형식 변경

strptime 함수에 사용되는 형식을 문자열에 맞게 수정하여 시간 정보까지 포함하도록 함

기존 코드
# 'YYYY-MM-DD' 형식으로 날짜와 시간 모두를 해석
send_date = datetime.strptime(start, "%Y-%m-%d) - timedelta(days=3)
고친 코드
# 'YYYY-MM-DD HH:MM:SS' 형식으로 날짜와 시간 모두를 해석
send_date = datetime.strptime(start, "%Y-%m-%d %H:%M:%S") - timedelta(days=3)

3. tests.py 실행 과정 중 오류 발생

(venv) PS C:\django_calender\django_calender> python manage.py test calender.tests.EmailTest.test_send_email_function
Found 1 test(s).
Creating test database for alias 'default'...
Traceback (most recent call last):
  File "C:\django_calender\venv\Lib\site-packages\django\db\backends\utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\db\backends\mysql\base.py", line 75, in execute
    return self.cursor.execute(query, args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\MySQLdb\cursors.py", line 179, in execute
    res = self._query(mogrified_query)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\MySQLdb\cursors.py", line 330, in _query
    db.query(q)
  File "C:\django_calender\venv\Lib\site-packages\MySQLdb\connections.py", line 255, in query
    _mysql.connection.query(self, query)
MySQLdb.OperationalError: (1824, "Failed to open the referenced table 'auth_user'")

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\django_calender\django_calender\manage.py", line 22, in <module>
    main()
  File "C:\django_calender\django_calender\manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\commands\test.py", line 24, in run_from_argv
    super().run_from_argv(argv)
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\base.py", line 412, in run_from_argv
    self.execute(*args, **cmd_options)
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\base.py", line 458, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\commands\test.py", line 68, in handle
    failures = test_runner.run_tests(test_labels)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\test\runner.py", line 1054, in run_tests
    old_config = self.setup_databases(
                 ^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\test\runner.py", line 950, in setup_databases
    return _setup_databases(
           ^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\test\utils.py", line 221, in setup_databases
    connection.creation.create_test_db(
  File "C:\django_calender\venv\Lib\site-packages\django\db\backends\base\creation.py", line 78, in create_test_db
    call_command(
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\__init__.py", line 194, in call_command
    return command.execute(*args, **defaults)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\base.py", line 458, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\base.py", line 106, in wrapper
    res = handle_func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\commands\migrate.py", line 321, in handle
    self.sync_apps(connection, executor.loader.unmigrated_apps)
  File "C:\django_calender\venv\Lib\site-packages\django\core\management\commands\migrate.py", line 468, in sync_apps
    with connection.schema_editor() as editor:
  File "C:\django_calender\venv\Lib\site-packages\django\db\backends\base\schema.py", line 166, in __exit__
    self.execute(sql)
  File "C:\django_calender\venv\Lib\site-packages\django\db\backends\base\schema.py", line 201, in execute
    cursor.execute(sql, params)
    res = self._query(mogrified_query)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\django_calender\venv\Lib\site-packages\MySQLdb\cursors.py", line 330, in _query
    db.query(q)
  File "C:\django_calender\venv\Lib\site-packages\MySQLdb\connections.py", line 255, in query
    _mysql.connection.query(self, query)
django.db.utils.OperationalError: (1824, "Failed to open the referenced table 'auth_user'")

해결

C:\django_calender\django_calender\venv\Lib\site-packages\django\conf\global_settings.py

이 경로에서 
# Optional SMTP authentication information for EMAIL_HOST.
EMAIL_HOST_USER = "sw.project.django@gmail.com"
EMAIL_HOST_PASSWORD = "brdo ybhi dopd wibz"
EMAIL_USE_TLS = False
EMAIL_USE_SSL = False
EMAIL_SSL_CERTFILE = None
EMAIL_SSL_KEYFILE = None
EMAIL_TIMEOUT = None

4.

해결

공부한 내용

SMTP
인터넷 메일을 송수신 할 때는 SMTP (Simple Mail Transfer Protocol) 이라는 규약 사용

MIME
인터넷의 전자 메일에서 사용되는 문자 데이터를 표현하기 위한 형식 표준

SMTP 서버의 포트 587과 465의 차이

두 포트가 다른 유형의 보안 연결을 사용한다는 점에서 차이가 있습니다.

포트 587: 이 포트는 일반적으로 메일 전송을 위한 '시작할 TLS'(StartTLS) 연결에 사용됩니다. StartTLS는 연결이 시작된 후 보안 연결로 "업그레이드"되는 방식입니다. 즉, 서버와 클라이언트 간의 초기 연결은 일반 텍스트로 시작되며, STARTTLS 명령이 전송된 후 TLS 보안 연결로 전환됩니다.

포트 465: 이 포트는 'SSL/TLS' 연결에 사용됩니다. SSL/TLS를 사용하는 경우, 연결 자체가 처음부터 보안 연결로 시작됩니다. 즉, 모든 통신이 처음부터 암호화되어 전송됩니다.

두 방식 모두 이메일 전송을 보안적으로 처리하기 위한 방법이지만, 포트 587과 STARTTLS 방식이 더 널리 사용됩니다.

python manage.py shell
from django.db import connections
from django.db.utils import OperationalError

db_conn = connections['default']
try:
    c = db_conn.cursor()
except OperationalError:
    print("연결 실패")
else:
    print("연결 성공")

다음 주차 활동 계획

profile
컴퓨터정보과

0개의 댓글