diff --git a/cert/backend/.env.example b/cert/backend/.env.example new file mode 100644 index 0000000..c13c356 --- /dev/null +++ b/cert/backend/.env.example @@ -0,0 +1,5 @@ +# LOG_LEVEL=INFO +# ENVIRONMENT=dev + +# CORS Origins (쉼표로 구분) +CORS_ORIGINS=https://cert.pseudo-lab.com,https://dev-cert.pseudolab-devfactory.com,http://localhost:5173 diff --git a/cert/backend/src/constants/error_codes.py b/cert/backend/src/constants/error_codes.py index ca1fce2..84c6905 100644 --- a/cert/backend/src/constants/error_codes.py +++ b/cert/backend/src/constants/error_codes.py @@ -5,7 +5,10 @@ class ErrorCodes: class ErrorMessages: """에러 메시지 상수""" - NO_HISTORY = "수료이력이 확인되지 않습니다. 🥲\n디스코드를 통해 김찬란에게 문의해주세요." + NO_HISTORY = "수료 명단에 존재하지 않습니다. 🥲\n입력하신 이름 '{name}'(이)가 명단에 있는지 확인하거나, 스터디 빌더 혹은 질문게시판에 문의해주세요." + USER_DROPPED_OUT = "스터디를 수료하지 않았습니다. 🥲\n스터디 빌더 혹은 질문게시판에 문의해주세요." + STUDY_NOT_COMPLETED = "수료증은 스터디가 완료된 이후 발급 가능합니다.\n스터디 빌더 혹은 질문게시판에 문의해주세요." + PROJECT_NOT_FOUND = "해당 프로젝트가 검색되지 않습니다.\n기수와 스터디명을 다시 확인하거나, 스터디 빌더 혹은 질문게시판에 문의해주세요." PIPELINE_ERROR = "발급 처리 중 오류가 발생했습니다." CONTACT_INFO = "시스템 상의 오류로 수료증 발급에 실패했습니다. 🥲\n디스코드를 통해 김수현(kyopbi)에게 문의해주세요" diff --git a/cert/backend/src/main.py b/cert/backend/src/main.py index e024221..662b3d3 100644 --- a/cert/backend/src/main.py +++ b/cert/backend/src/main.py @@ -40,11 +40,23 @@ def configure_logging() -> None: # Access log middleware app.middleware("http")(access_log_middleware) -# CORS 미들웨어 설정 -origins = os.getenv("CORS_ORIGINS", "").split(",") +# CORS configuration +# Load allowed origins from CORS_ORIGINS environment variable (comma-separated) +cors_origins_str = os.getenv("CORS_ORIGINS", "") +if cors_origins_str: + origins = [origin.strip() for origin in cors_origins_str.split(",") if origin.strip()] +else: + # Default origins for local development and known production/dev domains + origins = [ + "http://localhost:3000", + "http://localhost:5173", + "https://cert.pseudo-lab.com", + "https://dev-cert.pseudolab-devfactory.com", + ] + app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/cert/backend/src/services/certificate_service.py b/cert/backend/src/services/certificate_service.py index 9d180b6..0404e61 100644 --- a/cert/backend/src/services/certificate_service.py +++ b/cert/backend/src/services/certificate_service.py @@ -589,6 +589,15 @@ async def _create_new_certificate( ) ) + except NotEligibleError as e: + # 수료 대상 아님 (명단에 없거나 이탈자 등) + logger.warning(f"수료 대상 아님: {str(e)}") + if request_id: + await notion_client.update_certificate_status( + page_id=request_id, + status=CertificateStatus.NOT_ELIGIBLE + ) + raise e except Exception as e: # 시스템 오류 logger.exception("신규 수료증 발급 중 시스템 오류") diff --git a/cert/backend/src/utils/notion_client.py b/cert/backend/src/utils/notion_client.py index 50c29d9..1b52c6f 100644 --- a/cert/backend/src/utils/notion_client.py +++ b/cert/backend/src/utils/notion_client.py @@ -6,7 +6,7 @@ import aiohttp from typing import Optional, Dict, Any, List -from ..constants.error_codes import NotEligibleError, ResponseStatus +from ..constants.error_codes import NotEligibleError, ResponseStatus, ErrorMessages from ..models.certificate import CertificateStatus from ..models.project import Project, SeasonGroup, ProjectsBySeasonResponse @@ -156,14 +156,17 @@ async def verify_user_participation( user_role = "BUILDER" elif user_name in runner_names: user_role = "RUNNER" + # '수료자' 필드에 이름이 포함되어 있는지 확인 + elif any(user_name in c for c in completer_names): + user_role = "RUNNER" # 2. 사용자가 이탈자에 있는지 확인 if user_name in dropout_names: - raise NotEligibleError(f"수료 명단에 존재하지 않습니다. 🥲\n디스코드를 통해 질문게시판에 문의해주세요.") + raise NotEligibleError(ErrorMessages.USER_DROPPED_OUT) # 3. 사용자가 참여자 목록에 있는지 확인 if user_role is None: - raise NotEligibleError(f"수료 명단에 존재하지 않습니다. 🥲\n디스코드를 통해 질문게시판에 문의해주세요.") + raise NotEligibleError(ErrorMessages.NO_HISTORY.format(name=user_name)) study_status = properties.get("단계", {}).get("select", {}) period_raw = project.get("properties", {}).get("기간", {}).get("date", {}) or {} @@ -174,9 +177,7 @@ async def verify_user_participation( ) if study_status.get("name") != "완료": - raise NotEligibleError( - "수료증은 스터디가 완료된 이후 발급 가능합니다.\n디스코드를 통해 질문게시판에 문의해주세요." - ) + raise NotEligibleError(ErrorMessages.STUDY_NOT_COMPLETED) fallback_period = self.default_periods.get(str(season), {}) raw_start = period_raw.get("start") @@ -273,7 +274,7 @@ async def verify_user_participation( "course_name": course_name, }, ) - raise Exception("해당 프로젝트가 검색되지 않습니다. \n디스코드를 통해 질문게시판에 문의해주세요.") + raise NotEligibleError(ErrorMessages.PROJECT_NOT_FOUND) except Exception as e: raise e