From f7bb8114dafa14b547e94eb73f07b705c051d700 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:31:24 +0900 Subject: [PATCH 01/14] chore: update pyproject.toml and CI workflow --- .github/workflows/ci.yml | 62 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 2 ++ pyproject.toml | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..88ccbcd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +# .github/workflows/ci.yml + +name: CI Pipeline # GitHub Actions 워크플로우 이름 + +on: + push: + branches: + - master # master에 push되면 실행 + - dev # dev 브랜치에 push되면 실행 + pull_request: + branches: + - master # master로 PR이 생성되면 실행 + - dev # dev로 PR이 생성되면 실행 + +jobs: + build: + runs-on: ubuntu-latest # 최신 Ubuntu 환경에서 실행 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + # 최신 코드 가져오기 (GitHub Actions에서 실행되는 CI 환경으로 코드 다운로드) + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" # Python 3.10 환경 설정 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip # 최신 pip 설치 + pip install -e .[dev,test] # pyproject.toml 기반 패키지 설치 + + - name: Run Lint (flake8) + run: flake8 . + # 코드 스타일 체크 + + - name: Run Formatting Check (black) + run: black --check . + # 코드 포맷 검사 + + - name: Run Tests (pytest) + run: pytest --cov=src + # 테스트 실행 및 커버리지 측정 + + - name: Upload Coverage Report + uses: codecov/codecov-action@v3 + # 테스트 커버리지 리포트 업로드 + + deploy: + needs: build + if: github.ref == 'refs/heads/master' # master 브랜치에 push될 때만 실행 + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Deploy to Production + run: | + echo "Deploying application..." + # 여기에 배포 스크립트 추가 (예: AWS S3, Lambda, ECS 등) diff --git a/.gitignore b/.gitignore index 7b50d7d..b24f182 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,5 @@ Thumbs.db config.local.py *.env.local node_modules/ + +dailydevq/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..db0ae34 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +# 빌드 시스템 설정 (Python 패키징 표준 PEP 518) +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +# 프로젝트 기본 정보 +[project] +name = "dailydevq" +version = "0.1.0" +description = "A technical interview preparation tool" +dependencies = [ + "Flask==3.1.0", + "blinker==1.9.0", + "click==8.1.7", + "itsdangerous==2.2.0", + "Jinja2==3.1.5", + "MarkupSafe==3.0.2", + "Werkzeug==3.1.3", + "python-dotenv==1.0.1", + "typing_extensions==4.12.2", + "boto3==1.35.63", + "botocore==1.35.63", + "jmespath==1.0.1", + "python-dateutil==2.9.0.post0", + "six==1.16.0", + "s3transfer==0.10.3", + "flask-bootstrap==3.3.7", + "flask_login==0.6.3", + "openai==1.58.1" +] + +# 선택적 의존성 그룹 (개발 및 테스트 환경) +[project.optional-dependencies] +dev = [ + "black==24.10.0", + "flake8==7.1.1", + "mccabe==0.7.0", + "mypy-extensions==1.0.0", + "packaging==24.2", + "pathspec==0.12.1", + "platformdirs==4.3.6", + "pycodestyle==2.12.1", + "pyflakes==3.2.0" +] +test = [ + "pytest==8.3.3", + "exceptiongroup==1.2.2", + "iniconfig==2.0.0", + "pluggy==1.5.0", + "tomli==2.1.0" +] + +# CLI 실행 파일 설정 (패키지 설치 후 실행 가능) +[project.scripts] +dailydevq = "dailydevq.cli:main" From 413f4a5fb406dc593f6427cbdcb08878c53555cd Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:35:14 +0900 Subject: [PATCH 02/14] chore: remove old requirements files --- requirements.txt | 1 - requirements/base.txt | 18 ------------- requirements/dev.txt | 10 ------- requirements/test.txt | 6 ----- requirements_split.py | 62 ------------------------------------------- 5 files changed, 97 deletions(-) delete mode 100644 requirements.txt delete mode 100644 requirements/base.txt delete mode 100644 requirements/dev.txt delete mode 100644 requirements/test.txt delete mode 100644 requirements_split.py diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5603c37..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --r requirements/base.txt diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index 3158271..0000000 --- a/requirements/base.txt +++ /dev/null @@ -1,18 +0,0 @@ -Flask==3.1.0 -blinker==1.9.0 -click==8.1.7 -itsdangerous==2.2.0 -Jinja2==3.1.5 -MarkupSafe==3.0.2 -Werkzeug==3.1.3 -python-dotenv==1.0.1 -typing_extensions==4.12.2 -boto3==1.35.63 -botocore==1.35.63 -jmespath==1.0.1 -python-dateutil==2.9.0.post0 -six==1.16.0 -s3transfer==0.10.3 -flask-bootstrap==3.3.7 -flask_login==0.6.3 -openai==1.58.1 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 047b3db..0000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,10 +0,0 @@ --r base.txt -black==24.10.0 -flake8==7.1.1 -mccabe==0.7.0 -mypy-extensions==1.0.0 -packaging==24.2 -pathspec==0.12.1 -platformdirs==4.3.6 -pycodestyle==2.12.1 -pyflakes==3.2.0 diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index 4fddbe9..0000000 --- a/requirements/test.txt +++ /dev/null @@ -1,6 +0,0 @@ --r base.txt -pytest==8.3.3 -exceptiongroup==1.2.2 -iniconfig==2.0.0 -pluggy==1.5.0 -tomli==2.1.0 diff --git a/requirements_split.py b/requirements_split.py deleted file mode 100644 index d1b71d5..0000000 --- a/requirements_split.py +++ /dev/null @@ -1,62 +0,0 @@ -# ./requirements_split.py - -import os - -# 기본 패키지 (Flask 관련 핵심 패키지) -base_packages = """Flask==3.1.0 -blinker==1.9.0 -click==8.1.7 -itsdangerous==2.2.0 -Jinja2==3.1.4 -MarkupSafe==3.0.2 -Werkzeug==3.1.3 -python-dotenv==1.0.1 -typing_extensions==4.12.2 -boto3==1.35.63 -botocore==1.35.63 -jmespath==1.0.1 -python-dateutil==2.9.0.post0 -six==1.16.0 -s3transfer==0.10.3 -flask-bootstrap==3.3.7 -flask_login==0.6.3 -""" - -# 개발용 패키지 (코드 포맷팅, 린팅 도구) -development_packages = """black==24.10.0 -flake8==7.1.1 -mccabe==0.7.0 -mypy-extensions==1.0.0 -packaging==24.2 -pathspec==0.12.1 -platformdirs==4.3.6 -pycodestyle==2.12.1 -pyflakes==3.2.0 -""" - -# 테스트용 패키지 -testing_packages = """pytest==8.3.3 -exceptiongroup==1.2.2 -iniconfig==2.0.0 -pluggy==1.5.0 -tomli==2.1.0 -""" - -# requirements 디렉토리 생성 -os.makedirs('requirements', exist_ok=True) - -# 각 파일 생성 및 내용 작성 -with open('requirements/base.txt', 'w') as f: - f.write(base_packages) - -with open('requirements/dev.txt', 'w') as f: - f.write("-r base.txt\n") # base.txt 의존성 포함 - f.write(development_packages) - -with open('requirements/test.txt', 'w') as f: - f.write("-r base.txt\n") # base.txt 의존성 포함 - f.write(testing_packages) - -# 프로덕션용 requirements.txt 업데이트 -with open('requirements.txt', 'w') as f: - f.write("-r requirements/base.txt\n") From ac3550acf1ce37301cc876a6cdbf7506e8b4635a Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:46:11 +0900 Subject: [PATCH 03/14] chore: update pyproject.toml for src-based package discovery --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index db0ae34..5945b6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,10 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" +# 패키지 디스커버리 설정 (setuptools.find_packages 사용) +[tool.setuptools.packages.find] +where = ["src"] # src 폴더 내부에서 패키지 탐색 + # 프로젝트 기본 정보 [project] name = "dailydevq" From 8e4ee7923241ce1d9ad79da2753c4cd0be90fb2f Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:46:59 +0900 Subject: [PATCH 04/14] refactor: move backend and frontend into src directory for better package management --- {backend => src/backend}/functions/__init__.py | 0 {backend => src/backend}/functions/email_sender/handler.py | 0 {backend => src/backend}/functions/user_service.py | 0 {frontend => src/frontend}/app/__init__.py | 0 {frontend => src/frontend}/app/app.py | 0 {frontend => src/frontend}/app/handlers.py | 0 {frontend => src/frontend}/app/routes.py | 0 {frontend => src/frontend}/app/static/css/dashboard.css | 0 {frontend => src/frontend}/app/static/css/login.css | 0 {frontend => src/frontend}/app/static/css/style.css | 0 {frontend => src/frontend}/app/static/images/github-icon.svg | 0 {frontend => src/frontend}/app/static/images/google-icon.svg | 0 {frontend => src/frontend}/app/static/images/kakao-icon.svg | 0 {frontend => src/frontend}/app/static/images/naver-icon.svg | 0 {frontend => src/frontend}/app/static/js/login.js | 0 {frontend => src/frontend}/app/templates/404.html | 0 {frontend => src/frontend}/app/templates/500.html | 0 {frontend => src/frontend}/app/templates/base.html | 0 {frontend => src/frontend}/app/templates/dashboard.html | 0 .../frontend}/app/templates/dashboard/generate_question.html | 0 {frontend => src/frontend}/app/templates/login.html | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename {backend => src/backend}/functions/__init__.py (100%) rename {backend => src/backend}/functions/email_sender/handler.py (100%) rename {backend => src/backend}/functions/user_service.py (100%) rename {frontend => src/frontend}/app/__init__.py (100%) rename {frontend => src/frontend}/app/app.py (100%) rename {frontend => src/frontend}/app/handlers.py (100%) rename {frontend => src/frontend}/app/routes.py (100%) rename {frontend => src/frontend}/app/static/css/dashboard.css (100%) rename {frontend => src/frontend}/app/static/css/login.css (100%) rename {frontend => src/frontend}/app/static/css/style.css (100%) rename {frontend => src/frontend}/app/static/images/github-icon.svg (100%) rename {frontend => src/frontend}/app/static/images/google-icon.svg (100%) rename {frontend => src/frontend}/app/static/images/kakao-icon.svg (100%) rename {frontend => src/frontend}/app/static/images/naver-icon.svg (100%) rename {frontend => src/frontend}/app/static/js/login.js (100%) rename {frontend => src/frontend}/app/templates/404.html (100%) rename {frontend => src/frontend}/app/templates/500.html (100%) rename {frontend => src/frontend}/app/templates/base.html (100%) rename {frontend => src/frontend}/app/templates/dashboard.html (100%) rename {frontend => src/frontend}/app/templates/dashboard/generate_question.html (100%) rename {frontend => src/frontend}/app/templates/login.html (100%) diff --git a/backend/functions/__init__.py b/src/backend/functions/__init__.py similarity index 100% rename from backend/functions/__init__.py rename to src/backend/functions/__init__.py diff --git a/backend/functions/email_sender/handler.py b/src/backend/functions/email_sender/handler.py similarity index 100% rename from backend/functions/email_sender/handler.py rename to src/backend/functions/email_sender/handler.py diff --git a/backend/functions/user_service.py b/src/backend/functions/user_service.py similarity index 100% rename from backend/functions/user_service.py rename to src/backend/functions/user_service.py diff --git a/frontend/app/__init__.py b/src/frontend/app/__init__.py similarity index 100% rename from frontend/app/__init__.py rename to src/frontend/app/__init__.py diff --git a/frontend/app/app.py b/src/frontend/app/app.py similarity index 100% rename from frontend/app/app.py rename to src/frontend/app/app.py diff --git a/frontend/app/handlers.py b/src/frontend/app/handlers.py similarity index 100% rename from frontend/app/handlers.py rename to src/frontend/app/handlers.py diff --git a/frontend/app/routes.py b/src/frontend/app/routes.py similarity index 100% rename from frontend/app/routes.py rename to src/frontend/app/routes.py diff --git a/frontend/app/static/css/dashboard.css b/src/frontend/app/static/css/dashboard.css similarity index 100% rename from frontend/app/static/css/dashboard.css rename to src/frontend/app/static/css/dashboard.css diff --git a/frontend/app/static/css/login.css b/src/frontend/app/static/css/login.css similarity index 100% rename from frontend/app/static/css/login.css rename to src/frontend/app/static/css/login.css diff --git a/frontend/app/static/css/style.css b/src/frontend/app/static/css/style.css similarity index 100% rename from frontend/app/static/css/style.css rename to src/frontend/app/static/css/style.css diff --git a/frontend/app/static/images/github-icon.svg b/src/frontend/app/static/images/github-icon.svg similarity index 100% rename from frontend/app/static/images/github-icon.svg rename to src/frontend/app/static/images/github-icon.svg diff --git a/frontend/app/static/images/google-icon.svg b/src/frontend/app/static/images/google-icon.svg similarity index 100% rename from frontend/app/static/images/google-icon.svg rename to src/frontend/app/static/images/google-icon.svg diff --git a/frontend/app/static/images/kakao-icon.svg b/src/frontend/app/static/images/kakao-icon.svg similarity index 100% rename from frontend/app/static/images/kakao-icon.svg rename to src/frontend/app/static/images/kakao-icon.svg diff --git a/frontend/app/static/images/naver-icon.svg b/src/frontend/app/static/images/naver-icon.svg similarity index 100% rename from frontend/app/static/images/naver-icon.svg rename to src/frontend/app/static/images/naver-icon.svg diff --git a/frontend/app/static/js/login.js b/src/frontend/app/static/js/login.js similarity index 100% rename from frontend/app/static/js/login.js rename to src/frontend/app/static/js/login.js diff --git a/frontend/app/templates/404.html b/src/frontend/app/templates/404.html similarity index 100% rename from frontend/app/templates/404.html rename to src/frontend/app/templates/404.html diff --git a/frontend/app/templates/500.html b/src/frontend/app/templates/500.html similarity index 100% rename from frontend/app/templates/500.html rename to src/frontend/app/templates/500.html diff --git a/frontend/app/templates/base.html b/src/frontend/app/templates/base.html similarity index 100% rename from frontend/app/templates/base.html rename to src/frontend/app/templates/base.html diff --git a/frontend/app/templates/dashboard.html b/src/frontend/app/templates/dashboard.html similarity index 100% rename from frontend/app/templates/dashboard.html rename to src/frontend/app/templates/dashboard.html diff --git a/frontend/app/templates/dashboard/generate_question.html b/src/frontend/app/templates/dashboard/generate_question.html similarity index 100% rename from frontend/app/templates/dashboard/generate_question.html rename to src/frontend/app/templates/dashboard/generate_question.html diff --git a/frontend/app/templates/login.html b/src/frontend/app/templates/login.html similarity index 100% rename from frontend/app/templates/login.html rename to src/frontend/app/templates/login.html From 18f5153389b1b637d78ed7f7190afda3cfd1bdd7 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:01:50 +0900 Subject: [PATCH 05/14] chore: add flake8 configuration to ignore virtual env and extend line length --- .flake8 | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5cac886 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = .venv,venv,env,__pycache__,site-packages,dailydevq/lib +max-line-length = 120 From d748dec54e89472f685e05c23ba228fafeafc305 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:03:10 +0900 Subject: [PATCH 06/14] style: apply Flake8 formatting to Python files --- src/backend/functions/email_sender/handler.py | 5 +- src/backend/functions/user_service.py | 9 +- src/frontend/app/app.py | 293 ++++++++++-------- src/frontend/app/handlers.py | 6 +- src/frontend/app/routes.py | 24 +- 5 files changed, 194 insertions(+), 143 deletions(-) diff --git a/src/backend/functions/email_sender/handler.py b/src/backend/functions/email_sender/handler.py index 6507b6d..59619b9 100644 --- a/src/backend/functions/email_sender/handler.py +++ b/src/backend/functions/email_sender/handler.py @@ -1,7 +1,4 @@ # backend/functions/email_sender/handler.py def lambda_handler(event, context): # 이메일 발송 로직 - return { - 'statusCode': 200, - 'body': 'Email sent successfully' - } \ No newline at end of file + return {"statusCode": 200, "body": "Email sent successfully"} diff --git a/src/backend/functions/user_service.py b/src/backend/functions/user_service.py index ed6d98e..cfbb2ec 100644 --- a/src/backend/functions/user_service.py +++ b/src/backend/functions/user_service.py @@ -8,13 +8,14 @@ # DynamoDB 클라이언트 생성 dynamodb = boto3.resource( - "dynamodb", + "dynamodb", region_name="ap-northeast-2", - aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'), - aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY') + aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), ) users_table = dynamodb.Table("Users") # 테이블 이름 확인 + def save_user(email, name, profile_url, login_type): """ DynamoDB에 사용자 정보를 저장합니다. @@ -67,6 +68,7 @@ def save_user(email, name, profile_url, login_type): return user_data + def get_user_from_db(user_id): """ DynamoDB에서 사용자 정보를 검색합니다. @@ -111,6 +113,7 @@ def get_user_by_id(user_id): print(f"사용자 정보를 검색하는 중 오류가 발생했습니다: {e}") return None + def delete_user(user_id): """ 사용자 ID를 기준으로 사용자 정보를 삭제합니다. diff --git a/src/frontend/app/app.py b/src/frontend/app/app.py index ae48096..fafc557 100644 --- a/src/frontend/app/app.py +++ b/src/frontend/app/app.py @@ -7,7 +7,13 @@ # ========================================================================================= from flask import Flask, render_template, redirect, request, url_for -from flask_login import LoginManager, UserMixin, login_user, login_required, current_user +from flask_login import ( + LoginManager, + UserMixin, + login_user, + login_required, + current_user, +) from dotenv import load_dotenv import os import requests @@ -34,13 +40,13 @@ # 세션 관리를 위한 비밀키 설정 # Set a secret key for secure session management -app.secret_key = os.getenv('SECRET_KEY') +app.secret_key = os.getenv("SECRET_KEY") # 세션 쿠키 설정 (HTTPS 환경에서 SESSION_COOKIE_SECURE=True 권장) # Configure session cookies (SESSION_COOKIE_SECURE=True is recommended for HTTPS) -app.config['SESSION_COOKIE_SECURE'] = False -app.config['SESSION_COOKIE_HTTPONLY'] = True -app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' +app.config["SESSION_COOKIE_SECURE"] = False +app.config["SESSION_COOKIE_HTTPONLY"] = True +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # ========================================================================================= # Flask-Login Configuration (Flask-Login 설정) @@ -50,7 +56,10 @@ login_manager = LoginManager() login_manager.init_app(app) -login_manager.login_view = None # 기본 로그인 뷰 사용 안 함 / Disable the default login view +login_manager.login_view = ( + None # 기본 로그인 뷰 사용 안 함 / Disable the default login view +) + # Flask-Login용 사용자 모델 정의 # Define a user model class for Flask-Login @@ -60,6 +69,7 @@ def __init__(self, id, name, email): self.name = name self.email = email + # 로그인된 사용자 로드 함수 # This function loads a user from the database using email as an identifier @login_manager.user_loader @@ -67,15 +77,14 @@ def load_user(email): user_data = get_user_from_db(email) if user_data: return User( - id=user_data['email'], - name=user_data.get('name'), - email=user_data['email'] + id=user_data["email"], name=user_data.get("name"), email=user_data["email"] ) return None + # Blueprint 등록 # Register the main Blueprint with the Flask app -app.register_blueprint(main_bp, url_prefix='/') +app.register_blueprint(main_bp, url_prefix="/") # ========================================================================================= # Environment Variable Utility (환경 변수 유틸리티 함수) @@ -83,36 +92,41 @@ def load_user(email): # - Retrieve an environment variable; raise an error if it's not set. # ========================================================================================= + def get_env_var(name): value = os.getenv(name) if not value: raise RuntimeError(f"환경 변수 {name}이(가) 설정되지 않았습니다.") return value + # ========================================================================================= # DynamoDB: User Retrieval (DynamoDB에서 사용자 검색) # - 이메일을 이용해 DynamoDB 테이블에서 사용자를 조회하는 로직을 담고 있습니다. # - Use email as a key to retrieve user data from the 'Users' table in DynamoDB. # ========================================================================================= -def get_user_from_db(email): + +"""def get_user_from_db(email): import boto3 - dynamodb = boto3.resource('dynamodb') - table = dynamodb.Table('Users') # 실제 DynamoDB 테이블 이름과 일치해야 합니다. + + dynamodb = boto3.resource("dynamodb") + table = dynamodb.Table("Users") # 실제 DynamoDB 테이블 이름과 일치해야 합니다. try: # 이메일을 기준으로 테이블에서 스캔 # Scan the table for the item with the matching email response = table.scan( FilterExpression="email = :email", - ExpressionAttributeValues={":email": email} + ExpressionAttributeValues={":email": email}, ) - items = response.get('Items') + items = response.get("Items") if items: return items[0] # 첫 번째 매칭된 결과 반환 / Return the first matched item return None except Exception as e: print(f"DynamoDB 오류: {e}") return None +""" # ========================================================================================= # OAuth Client Settings (OAuth 클라이언트 설정) @@ -120,18 +134,18 @@ def get_user_from_db(email): # - Retrieve OAuth settings from environment variables for Google, GitHub, Kakao, and Naver. # ========================================================================================= -GOOGLE_CLIENT_ID = get_env_var('GOOGLE_CLIENT_ID') -GOOGLE_CLIENT_SECRET = get_env_var('GOOGLE_CLIENT_SECRET') -GITHUB_CLIENT_ID = get_env_var('GITHUB_CLIENT_ID') -GITHUB_CLIENT_SECRET = get_env_var('GITHUB_CLIENT_SECRET') -KAKAO_CLIENT_ID = get_env_var('KAKAO_CLIENT_ID') -NAVER_CLIENT_ID = get_env_var('NAVER_CLIENT_ID') -NAVER_CLIENT_SECRET = get_env_var('NAVER_CLIENT_SECRET') +GOOGLE_CLIENT_ID = get_env_var("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = get_env_var("GOOGLE_CLIENT_SECRET") +GITHUB_CLIENT_ID = get_env_var("GITHUB_CLIENT_ID") +GITHUB_CLIENT_SECRET = get_env_var("GITHUB_CLIENT_SECRET") +KAKAO_CLIENT_ID = get_env_var("KAKAO_CLIENT_ID") +NAVER_CLIENT_ID = get_env_var("NAVER_CLIENT_ID") +NAVER_CLIENT_SECRET = get_env_var("NAVER_CLIENT_SECRET") -GOOGLE_REDIRECT_URI = get_env_var('GOOGLE_REDIRECT_URI') -GITHUB_REDIRECT_URI = get_env_var('GITHUB_REDIRECT_URI') -KAKAO_REDIRECT_URI = get_env_var('KAKAO_REDIRECT_URI') -NAVER_REDIRECT_URI = get_env_var('NAVER_REDIRECT_URI') +GOOGLE_REDIRECT_URI = get_env_var("GOOGLE_REDIRECT_URI") +GITHUB_REDIRECT_URI = get_env_var("GITHUB_REDIRECT_URI") +KAKAO_REDIRECT_URI = get_env_var("KAKAO_REDIRECT_URI") +NAVER_REDIRECT_URI = get_env_var("NAVER_REDIRECT_URI") # ========================================================================================= # User Information Handling (사용자 정보 처리) @@ -139,6 +153,7 @@ def get_user_from_db(email): # - Store the user info retrieved from the OAuth flow to the database and log them in. # ========================================================================================= + def save_user_info(email, name, profile_url, provider): """ 사용자가 DB에 존재하지 않으면 저장하고, Flask-Login을 통해 로그인 처리합니다. @@ -154,6 +169,7 @@ def save_user_info(email, name, profile_url, provider): user = User(id=email, name=name, email=email) login_user(user) + def fetch_user_info(token_url, token_data, user_info_url, headers=None): """ 토큰을 발급받고(access token), 해당 토큰으로 사용자 정보를 조회하는 공통 함수입니다. @@ -167,57 +183,63 @@ def fetch_user_info(token_url, token_data, user_info_url, headers=None): # 액세스 토큰 요청 / Obtain access token token_response = requests.post(token_url, data=token_data) token_response.raise_for_status() - access_token = token_response.json().get('access_token') + access_token = token_response.json().get("access_token") # 사용자 정보 요청 / Request user info user_info_response = requests.get( user_info_url, - headers={**(headers or {}), 'Authorization': f'Bearer {access_token}'} + headers={**(headers or {}), "Authorization": f"Bearer {access_token}"}, ) user_info_response.raise_for_status() return user_info_response.json() except requests.exceptions.RequestException as e: raise RuntimeError(f"OAuth 인증 중 오류가 발생했습니다: {e}") + # ========================================================================================= # Routes (라우트) # - Flask 애플리케이션에서 처리할 URL 경로와 함수를 정의합니다. # - Define the URL endpoints and their corresponding functions. # ========================================================================================= + # 메인 페이지: 로그인 화면을 렌더링 # Main page: renders the login template -@app.route('/') +@app.route("/") def index(): - return render_template('login.html') + return render_template("login.html") + # 대시보드: 로그인된 사용자만 접근 가능 # Dashboard: accessible only for logged-in users -@app.route('/dashboard') +@app.route("/dashboard") @login_required def dashboard(): - return render_template('dashboard.html', user=current_user) + return render_template("dashboard.html", user=current_user) + # ========================================================================================= # Google OAuth Flow # ========================================================================================= -@app.route('/login/google') + +@app.route("/login/google") def login_google(): """ 구글 인증 페이지로 리다이렉트합니다. Redirect user to Google's OAuth consent screen. """ google_auth_url = ( - 'https://accounts.google.com/o/oauth2/v2/auth?' - f'client_id={GOOGLE_CLIENT_ID}&' - f'redirect_uri={GOOGLE_REDIRECT_URI}&' - 'response_type=code&' - 'scope=email' + "https://accounts.google.com/o/oauth2/v2/auth?" + f"client_id={GOOGLE_CLIENT_ID}&" + f"redirect_uri={GOOGLE_REDIRECT_URI}&" + "response_type=code&" + "scope=email" ) return redirect(google_auth_url) -@app.route('/login/google/callback') + +@app.route("/login/google/callback") def google_callback(): """ 구글 OAuth 콜백 처리: @@ -229,54 +251,60 @@ def google_callback(): 2) Fetch user info 3) Save user info and log in """ - code = request.args.get('code') + code = request.args.get("code") if not code: return "Google 인증 실패: 인증 코드를 찾을 수 없습니다.", 400 token_data = { - 'code': code, - 'client_id': GOOGLE_CLIENT_ID, - 'client_secret': GOOGLE_CLIENT_SECRET, - 'redirect_uri': GOOGLE_REDIRECT_URI, - 'grant_type': 'authorization_code' + "code": code, + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "redirect_uri": GOOGLE_REDIRECT_URI, + "grant_type": "authorization_code", } try: user_info = fetch_user_info( - token_url='https://oauth2.googleapis.com/token', + token_url="https://oauth2.googleapis.com/token", token_data=token_data, - user_info_url='https://www.googleapis.com/oauth2/v2/userinfo' + user_info_url="https://www.googleapis.com/oauth2/v2/userinfo", ) except RuntimeError as e: return str(e), 500 - email = user_info.get('email') + email = user_info.get("email") if not email: return "Google 인증 실패: 이메일 정보가 없습니다.", 400 - save_user_info(email, user_info.get('name'), user_info.get('picture'), "Google") - return redirect(url_for('main.dashboard')) + save_user_info(email, user_info.get("name"), user_info.get("picture"), "Google") + return redirect(url_for("main.dashboard")) + # ========================================================================================= # GitHub OAuth Flow # ========================================================================================= -@app.route('/login/github') + +@app.route("/login/github") def login_github(): """ 깃허브 인증 페이지로 리다이렉트합니다. Redirect user to GitHub's OAuth consent screen. """ from urllib.parse import urlencode - query_params = urlencode({ - 'client_id': GITHUB_CLIENT_ID, - 'redirect_uri': GITHUB_REDIRECT_URI, - 'scope': 'user:email' - }) - github_auth_url = f'https://github.com/login/oauth/authorize?{query_params}' + + query_params = urlencode( + { + "client_id": GITHUB_CLIENT_ID, + "redirect_uri": GITHUB_REDIRECT_URI, + "scope": "user:email", + } + ) + github_auth_url = f"https://github.com/login/oauth/authorize?{query_params}" return redirect(github_auth_url) -@app.route('/login/github/callback') + +@app.route("/login/github/callback") def github_callback(): """ 깃허브 OAuth 콜백 처리: @@ -288,51 +316,57 @@ def github_callback(): 2) Fetch user info 3) Save user info and log in """ - code = request.args.get('code') + code = request.args.get("code") if not code: return "GitHub 인증 실패: code가 없습니다.", 400 # 액세스 토큰 요청 / Request an access token - token_url = 'https://github.com/login/oauth/access_token' + token_url = "https://github.com/login/oauth/access_token" token_data = { - 'client_id': GITHUB_CLIENT_ID, - 'client_secret': GITHUB_CLIENT_SECRET, - 'code': code, - 'redirect_uri': GITHUB_REDIRECT_URI + "client_id": GITHUB_CLIENT_ID, + "client_secret": GITHUB_CLIENT_SECRET, + "code": code, + "redirect_uri": GITHUB_REDIRECT_URI, } - token_headers = {'Accept': 'application/json'} + token_headers = {"Accept": "application/json"} try: - token_response = requests.post(token_url, data=token_data, headers=token_headers) + token_response = requests.post( + token_url, data=token_data, headers=token_headers + ) token_response.raise_for_status() token_json = token_response.json() except requests.RequestException as e: return f"GitHub 인증 실패: {e}", 500 - access_token = token_json.get('access_token') + access_token = token_json.get("access_token") if not access_token: return f"GitHub 인증 실패: {token_json}", 500 # 깃허브 사용자 정보 요청 / Retrieve GitHub user information - user_info_url = 'https://api.github.com/user' + user_info_url = "https://api.github.com/user" try: - user_info_response = requests.get(user_info_url, headers={'Authorization': f'token {access_token}'}) + user_info_response = requests.get( + user_info_url, headers={"Authorization": f"token {access_token}"} + ) user_info_response.raise_for_status() user_info = user_info_response.json() except requests.RequestException as e: return f"GitHub 사용자 정보 요청 실패: {e}", 500 # 이메일 가져오기 / Fetch email - email = user_info.get('email') + email = user_info.get("email") if not email: - emails_url = 'https://api.github.com/user/emails' + emails_url = "https://api.github.com/user/emails" try: - emails_response = requests.get(emails_url, headers={'Authorization': f'token {access_token}'}) + emails_response = requests.get( + emails_url, headers={"Authorization": f"token {access_token}"} + ) emails_response.raise_for_status() emails = emails_response.json() for e in emails: - if e.get('primary') and e.get('verified'): - email = e.get('email') + if e.get("primary") and e.get("verified"): + email = e.get("email") break except requests.RequestException as e: return f"GitHub 이메일 요청 실패: {e}", 500 @@ -344,38 +378,41 @@ def github_callback(): try: save_user_info( email=email, - name=user_info.get('name', 'Unknown'), - profile_url=user_info.get('avatar_url', ''), - provider="GitHub" + name=user_info.get("name", "Unknown"), + profile_url=user_info.get("avatar_url", ""), + provider="GitHub", ) except Exception as e: return f"GitHub 인증 실패: 사용자 정보 저장 중 오류 발생. 상세: {e}", 500 # 사용자 객체 생성 및 로그인 / Create user object and log in - user = User(id=email, name=user_info.get('name', 'Unknown'), email=email) + user = User(id=email, name=user_info.get("name", "Unknown"), email=email) login_user(user) - return redirect(url_for('main.dashboard')) + return redirect(url_for("main.dashboard")) + # ========================================================================================= # Kakao OAuth Flow # ========================================================================================= -@app.route('/login/kakao') + +@app.route("/login/kakao") def login_kakao(): """ 카카오 인증 페이지로 리다이렉트합니다. Redirect user to Kakao's OAuth consent screen. """ kakao_auth_url = ( - 'https://kauth.kakao.com/oauth/authorize?' - f'client_id={KAKAO_CLIENT_ID}&' - f'redirect_uri={KAKAO_REDIRECT_URI}&' - 'response_type=code' + "https://kauth.kakao.com/oauth/authorize?" + f"client_id={KAKAO_CLIENT_ID}&" + f"redirect_uri={KAKAO_REDIRECT_URI}&" + "response_type=code" ) return redirect(kakao_auth_url) -@app.route('/login/kakao/callback') + +@app.route("/login/kakao/callback") def kakao_callback(): """ 카카오 OAuth 콜백 처리: @@ -387,61 +424,66 @@ def kakao_callback(): 2) Fetch user info 3) Save user info and log in """ - code = request.args.get('code') + code = request.args.get("code") token_data = { - 'grant_type': 'authorization_code', - 'client_id': KAKAO_CLIENT_ID, - 'redirect_uri': KAKAO_REDIRECT_URI, - 'code': code + "grant_type": "authorization_code", + "client_id": KAKAO_CLIENT_ID, + "redirect_uri": KAKAO_REDIRECT_URI, + "code": code, } try: user_info = fetch_user_info( - token_url='https://kauth.kakao.com/oauth/token', + token_url="https://kauth.kakao.com/oauth/token", token_data=token_data, - user_info_url='https://kapi.kakao.com/v2/user/me' + user_info_url="https://kapi.kakao.com/v2/user/me", ) except RuntimeError as e: return str(e), 500 - kakao_account = user_info.get('kakao_account', {}) - email = kakao_account.get('email') + kakao_account = user_info.get("kakao_account", {}) + email = kakao_account.get("email") if not email: return "Kakao 인증 실패: 이메일 정보가 없습니다.", 400 save_user_info( email=email, - name=user_info.get('properties', {}).get('nickname'), - profile_url=user_info.get('properties', {}).get('profile_image'), - provider="Kakao" + name=user_info.get("properties", {}).get("nickname"), + profile_url=user_info.get("properties", {}).get("profile_image"), + provider="Kakao", ) - user = User(id=email, name=user_info.get('properties', {}).get('nickname'), email=email) + user = User( + id=email, name=user_info.get("properties", {}).get("nickname"), email=email + ) login_user(user) - return redirect(url_for('main.dashboard')) + return redirect(url_for("main.dashboard")) + # ========================================================================================= # Naver OAuth Flow # ========================================================================================= -@app.route('/login/naver') + +@app.route("/login/naver") def login_naver(): """ 네이버 인증 페이지로 리다이렉트합니다. (CSRF 방지를 위해 state 사용) Redirect user to Naver's OAuth consent screen using a random state for CSRF protection. """ - state = 'RANDOM_STATE_STRING' + state = "RANDOM_STATE_STRING" naver_auth_url = ( - 'https://nid.naver.com/oauth2.0/authorize?' - f'response_type=code&' - f'client_id={NAVER_CLIENT_ID}&' - f'redirect_uri={NAVER_REDIRECT_URI}&' - f'state={state}' + "https://nid.naver.com/oauth2.0/authorize?" + f"response_type=code&" + f"client_id={NAVER_CLIENT_ID}&" + f"redirect_uri={NAVER_REDIRECT_URI}&" + f"state={state}" ) return redirect(naver_auth_url) -@app.route('/login/naver/callback') + +@app.route("/login/naver/callback") def naver_callback(): """ 네이버 OAuth 콜백 처리: @@ -453,48 +495,49 @@ def naver_callback(): 2) Fetch user info 3) Save user info and log in """ - code = request.args.get('code') - state = request.args.get('state') + code = request.args.get("code") + state = request.args.get("state") token_data = { - 'grant_type': 'authorization_code', - 'client_id': NAVER_CLIENT_ID, - 'client_secret': NAVER_CLIENT_SECRET, - 'code': code, - 'state': state + "grant_type": "authorization_code", + "client_id": NAVER_CLIENT_ID, + "client_secret": NAVER_CLIENT_SECRET, + "code": code, + "state": state, } try: user_info = fetch_user_info( - token_url='https://nid.naver.com/oauth2.0/token', + token_url="https://nid.naver.com/oauth2.0/token", token_data=token_data, - user_info_url='https://openapi.naver.com/v1/nid/me' + user_info_url="https://openapi.naver.com/v1/nid/me", ) except RuntimeError as e: return str(e), 500 - response = user_info.get('response', {}) - email = response.get('email') + response = user_info.get("response", {}) + email = response.get("email") if not email: return "Naver 인증 실패: 이메일 정보가 없습니다.", 400 save_user_info( email=email, - name=response.get('name'), - profile_url=response.get('profile_image'), - provider="Naver" + name=response.get("name"), + profile_url=response.get("profile_image"), + provider="Naver", ) - return redirect(url_for('main.dashboard')) + return redirect(url_for("main.dashboard")) + # ========================================================================================= # Main Application Execution (메인 애플리케이션 실행) # ========================================================================================= -if __name__ == '__main__': +if __name__ == "__main__": # URL 매핑 구조를 출력 (디버깅용) # Print the URL map (for debugging) print(app.url_map) - + # FLASK_DEBUG 환경 변수를 확인하여 디버그 모드 설정 # Set debug mode based on FLASK_DEBUG environment variable - app.run(debug=os.getenv('FLASK_DEBUG', 'false').lower() == 'true') + app.run(debug=os.getenv("FLASK_DEBUG", "false").lower() == "true") diff --git a/src/frontend/app/handlers.py b/src/frontend/app/handlers.py index e4fbd0b..8613f7f 100644 --- a/src/frontend/app/handlers.py +++ b/src/frontend/app/handlers.py @@ -1,8 +1,10 @@ # frontend/app/handlers.py from flask import render_template + def handle_404(error): - return render_template('404.html'), 404 + return render_template("404.html"), 404 + def handle_500(error): - return render_template('500.html'), 500 \ No newline at end of file + return render_template("500.html"), 500 diff --git a/src/frontend/app/routes.py b/src/frontend/app/routes.py index bbe32c2..a5b232f 100644 --- a/src/frontend/app/routes.py +++ b/src/frontend/app/routes.py @@ -5,31 +5,35 @@ from flask import Blueprint, render_template, request from flask_login import current_user, login_required -bp = Blueprint('main', __name__) +bp = Blueprint("main", __name__) -@bp.route('/logout') + +@bp.route("/logout") def logout(): # 로그아웃 처리 후 메인 페이지로 리다이렉트 (예: index 페이지가 있다면 'main.index'로 대체) from flask_login import logout_user + logout_user() return "로그아웃 완료" # 또는 redirect(url_for('main.index')) -@bp.route('/dashboard') + +@bp.route("/dashboard") @login_required def dashboard(): - return render_template('dashboard.html', user=current_user) + return render_template("dashboard.html", user=current_user) + -@bp.route('/generate-question', methods=['GET', 'POST']) +@bp.route("/generate-question", methods=["GET", "POST"]) @login_required def generate_question(): """ OpenAI API를 통해 질문을 생성하는 예시 라우트 """ - openai.api_key = os.getenv('OPENAI_API_KEY', 'YOUR_DEFAULT_API_KEY') + openai.api_key = os.getenv("OPENAI_API_KEY", "YOUR_DEFAULT_API_KEY") generated_question = None - if request.method == 'POST': - user_input = request.form.get('tech_stack', 'Cloud') + if request.method == "POST": + user_input = request.form.get("tech_stack", "Cloud") prompt_text = f"다음 기술 스택 {user_input}에 대한 면접 질문을 생성해 주세요." response = openai.Completion.create( engine="text-davinci-003", @@ -39,4 +43,6 @@ def generate_question(): ) generated_question = response.choices[0].text.strip() - return render_template('dashboard/generate_question.html', question=generated_question) + return render_template( + "dashboard/generate_question.html", question=generated_question + ) From 093626e68542af3e0ae1343aa7967f424fe4f5a0 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:44:12 +0900 Subject: [PATCH 07/14] feat: add pytest-cov to dependencies --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5945b6f..66e9c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,10 @@ dev = [ "pyflakes==3.2.0" ] test = [ - "pytest==8.3.3", + "pytest==8.3.4", "exceptiongroup==1.2.2", "iniconfig==2.0.0", + "pytest-cov==6.0.0", "pluggy==1.5.0", "tomli==2.1.0" ] From 127a885f9db6d71fa25926a8138b8c6115b90805 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:28:59 +0900 Subject: [PATCH 08/14] feat: pyproject.toml Edited(pytest, pytest-cov Version) --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 66e9c15..d5946db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,10 +47,12 @@ dev = [ "pyflakes==3.2.0" ] test = [ - "pytest==8.3.4", + # "pytest==8.3.4", + "pytest==7.2.0", "exceptiongroup==1.2.2", "iniconfig==2.0.0", - "pytest-cov==6.0.0", + # "pytest-cov==6.0.0", + "pytest-cov==5.0.0", "pluggy==1.5.0", "tomli==2.1.0" ] From 118097d2c59b73a2fc7c8fa963937e167e16fec2 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:31:47 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20=EC=83=81=EB=8C=80=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 66e9c15..fce6a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,5 @@ +# ./pyproject.toml + # 빌드 시스템 설정 (Python 패키징 표준 PEP 518) [build-system] requires = ["setuptools", "wheel"] From 89308b2d6dfbec81e100d07861feef2dfed1ad06 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:45:32 +0900 Subject: [PATCH 10/14] test: Mock AWS SES in test_email_sender.py to prevent real API calls --- pyproject.toml | 8 ++--- src/__init__.py | 0 src/backend/functions/email_sender/handler.py | 15 ++++++++-- src/tests/test_email_sender.py | 29 +++++++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/tests/test_email_sender.py diff --git a/pyproject.toml b/pyproject.toml index 7797f81..20c756c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,12 +49,12 @@ dev = [ "pyflakes==3.2.0" ] test = [ - # "pytest==8.3.4", - "pytest==7.2.0", + "pytest==8.3.4", + # "pytest==7.2.0", "exceptiongroup==1.2.2", "iniconfig==2.0.0", - # "pytest-cov==6.0.0", - "pytest-cov==5.0.0", + "pytest-cov==6.0.0", + # "pytest-cov==5.0.0", "pluggy==1.5.0", "tomli==2.1.0" ] diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/functions/email_sender/handler.py b/src/backend/functions/email_sender/handler.py index 59619b9..10775e1 100644 --- a/src/backend/functions/email_sender/handler.py +++ b/src/backend/functions/email_sender/handler.py @@ -1,4 +1,15 @@ # backend/functions/email_sender/handler.py + +import boto3 + def lambda_handler(event, context): - # 이메일 발송 로직 - return {"statusCode": 200, "body": "Email sent successfully"} + ses_client = boto3.client("ses") + response = ses_client.send_email( + Source="no-reply@example.com", + Destination={"ToAddresses": [event["to_email"]]}, + Message={ + "Subject": {"Data": event["subject"]}, + "Body": {"Text": {"Data": event["body"]}}, + }, + ) + return {"statusCode": 200, "body": response["MessageId"]} diff --git a/src/tests/test_email_sender.py b/src/tests/test_email_sender.py new file mode 100644 index 0000000..d3e090c --- /dev/null +++ b/src/tests/test_email_sender.py @@ -0,0 +1,29 @@ +# ./src/tests/test_email_sender.py + +import pytest +from unittest.mock import patch +from src.backend.functions.email_sender.handler import lambda_handler + +@patch("boto3.client") +def test_lambda_handler(mock_boto_client): + """Lambda 핸들러가 정상적으로 이메일을 발송하는지 테스트""" + + # Mock SES 클라이언트 설정 + mock_ses = mock_boto_client.return_value + mock_ses.send_email.return_value = {"MessageId": "12345"} + + # 이벤트 객체 (예제) + event = { + "to_email": "test@example.com", + "subject": "Test Subject", + "body": "This is a test email." + } + + response = lambda_handler(event, None) + + # 예상 결과 확인 + assert response["statusCode"] == 200 + assert response["body"] == "12345" + + # SES의 send_email 호출 여부 확인 + mock_ses.send_email.assert_called_once() From 045c83a1754f96f70b5bb51c366e412d27973a45 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:47:14 +0900 Subject: [PATCH 11/14] test: Mock AWS SES in test_email_sender.py to prevent real API calls --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..4584de7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests +pythonpath = . From ef5433b34c9c853450fadaa5582bcdf60d92cb19 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:50:46 +0900 Subject: [PATCH 12/14] style: Fix flake8 linting issues (E302, F401) --- src/backend/functions/email_sender/handler.py | 1 + src/tests/test_email_sender.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/functions/email_sender/handler.py b/src/backend/functions/email_sender/handler.py index 10775e1..09060fa 100644 --- a/src/backend/functions/email_sender/handler.py +++ b/src/backend/functions/email_sender/handler.py @@ -2,6 +2,7 @@ import boto3 + def lambda_handler(event, context): ses_client = boto3.client("ses") response = ses_client.send_email( diff --git a/src/tests/test_email_sender.py b/src/tests/test_email_sender.py index d3e090c..97b3ed2 100644 --- a/src/tests/test_email_sender.py +++ b/src/tests/test_email_sender.py @@ -1,9 +1,10 @@ # ./src/tests/test_email_sender.py -import pytest + from unittest.mock import patch from src.backend.functions.email_sender.handler import lambda_handler + @patch("boto3.client") def test_lambda_handler(mock_boto_client): """Lambda 핸들러가 정상적으로 이메일을 발송하는지 테스트""" From d09ec19379e0004475bc5133693d46efcbab3b73 Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:53:01 +0900 Subject: [PATCH 13/14] style: Format test_email_sender.py using black --- src/tests/test_email_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_email_sender.py b/src/tests/test_email_sender.py index 97b3ed2..3468f4a 100644 --- a/src/tests/test_email_sender.py +++ b/src/tests/test_email_sender.py @@ -17,7 +17,7 @@ def test_lambda_handler(mock_boto_client): event = { "to_email": "test@example.com", "subject": "Test Subject", - "body": "This is a test email." + "body": "This is a test email.", } response = lambda_handler(event, None) From d0c38872b3377ceb7ec15f5e281560be4e4d317a Mon Sep 17 00:00:00 2001 From: "Dong Hyeon, Shin" <109497684+dongkoony@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:56:00 +0900 Subject: [PATCH 14/14] refactor: Move AWS Terraform code to infrastructure/aws directory --- infrastructure/{ => aws}/dynamodb.tf | 0 infrastructure/{ => aws}/outputs.tf | 0 infrastructure/{ => aws}/providers.tf | 0 infrastructure/{ => aws}/variables.tf | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename infrastructure/{ => aws}/dynamodb.tf (100%) rename infrastructure/{ => aws}/outputs.tf (100%) rename infrastructure/{ => aws}/providers.tf (100%) rename infrastructure/{ => aws}/variables.tf (100%) diff --git a/infrastructure/dynamodb.tf b/infrastructure/aws/dynamodb.tf similarity index 100% rename from infrastructure/dynamodb.tf rename to infrastructure/aws/dynamodb.tf diff --git a/infrastructure/outputs.tf b/infrastructure/aws/outputs.tf similarity index 100% rename from infrastructure/outputs.tf rename to infrastructure/aws/outputs.tf diff --git a/infrastructure/providers.tf b/infrastructure/aws/providers.tf similarity index 100% rename from infrastructure/providers.tf rename to infrastructure/aws/providers.tf diff --git a/infrastructure/variables.tf b/infrastructure/aws/variables.tf similarity index 100% rename from infrastructure/variables.tf rename to infrastructure/aws/variables.tf