Skip to content

Commit 2692d49

Browse files
yesinkimsoohyunme
authored andcommitted
feat: 수료증 재발급 및 신규 발급 로직 추가
- 기존 수료증 확인 기능 추가: 이름, 코스명, 기수로 기존 수료증 검색 - 기존 수료증이 있을 경우 재발급 처리 로직 구현 - 신규 수료증 발급 로직 개선 및 오류 처리 강화 - NotionClient에 기존 수료증 확인 메서드 추가
1 parent fd5e6e4 commit 2692d49

File tree

2 files changed

+222
-15
lines changed

2 files changed

+222
-15
lines changed

cert/backend/src/services/certificate_service.py

Lines changed: 117 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import uuid
22
from datetime import datetime
33
from typing import Optional, List
4-
from fastapi import HTTPException
5-
6-
7-
84
from ..models.project import Project, ProjectsBySeasonResponse
95
from ..models.certificate import CertificateResponse, CertificateData, CertificateStatus, Role
10-
from ..constants.error_codes import ErrorCodes, ResponseStatus
116
from ..utils.notion_client import NotionClient
127
from ..utils.pdf_generator import PDFGenerator
138
from ..utils.email_sender import EmailSender
@@ -49,7 +44,122 @@ async def create_certificate(certificate_data: dict) -> CertificateResponse:
4944
notion_client = NotionClient()
5045

5146
try:
47+
# 1. 기존 수료증 확인 (재발급 여부 판단)
48+
existing_cert = await notion_client.check_existing_certificate(
49+
applicant_name=certificate_data["applicant_name"],
50+
course_name=certificate_data["course_name"],
51+
season=certificate_data["season"],
52+
recipient_email=certificate_data.get("recipient_email")
53+
)
54+
55+
# 기존 수료증 확인이 성공하고 기존 수료증이 있는 경우 재발급 처리
56+
if existing_cert and existing_cert.get("found"):
57+
print(f"기존 수료증 발견: {existing_cert.get('certificate_number')}")
58+
return await CertificateService._reissue_certificate(
59+
certificate_data, existing_cert, notion_client
60+
)
61+
62+
# 2. 신규 수료증 발급 처리
63+
return await CertificateService._create_new_certificate(
64+
certificate_data, notion_client
65+
)
66+
67+
except Exception as e:
68+
print(f"수료증 발급 중 오류: {e}")
69+
return CertificateResponse(
70+
status="500",
71+
message=f"수료증 발급 중 오류가 발생했습니다: {str(e)}",
72+
data=None
73+
)
74+
75+
@staticmethod
76+
async def _reissue_certificate(
77+
certificate_data: dict,
78+
existing_cert: dict,
79+
notion_client: NotionClient
80+
) -> CertificateResponse:
81+
"""기존 수료증 재발급"""
82+
try:
83+
# 기존 수료증 정보 사용
84+
existing_page_id = existing_cert.get("page_id")
85+
existing_cert_number = existing_cert.get("certificate_number")
86+
87+
print("🔄 기존 수료증 재발급 시작 (이름, 코스, 기수 일치):")
88+
print(f" - 기존 수료증 번호: '{existing_cert_number}'")
89+
print(f" - 요청 이메일: '{certificate_data.get('recipient_email', '')}'")
90+
91+
# 사용자 참여 이력 재확인 (역할 정보 가져오기)
92+
participation_info = await notion_client.verify_user_participation(
93+
user_name=certificate_data["applicant_name"],
94+
course_name=certificate_data["course_name"],
95+
season=certificate_data["season"]
96+
)
97+
98+
# 수료증 번호가 없는 경우 새로 생성
99+
if not existing_cert_number:
100+
print("⚠️ 기존 수료증에 번호가 없어 새로 생성합니다.")
101+
existing_cert_number = f"CERT-{datetime.now().year}{participation_info['project_code']}{str(uuid.uuid4())[:2].upper()}"
102+
print(f"🆕 새로 생성된 수료증 번호: {existing_cert_number}")
103+
104+
# PDF 수료증 재생성
105+
pdf_generator = PDFGenerator()
106+
pdf_bytes = pdf_generator.create_certificate(
107+
name=certificate_data["applicant_name"],
108+
season=certificate_data['season'],
109+
course_name=certificate_data["course_name"],
110+
role=participation_info["user_role"],
111+
period=participation_info["period"],
112+
)
52113

114+
# 이메일 재발송
115+
email_sender = EmailSender()
116+
await email_sender.send_certificate_email(
117+
recipient_email=certificate_data["recipient_email"],
118+
recipient_name=certificate_data["applicant_name"],
119+
course_name=certificate_data["course_name"],
120+
season=certificate_data["season"],
121+
role=participation_info["user_role"],
122+
certificate_bytes=pdf_bytes
123+
)
124+
125+
# 기존 수료증 상태를 재발급으로 업데이트
126+
await notion_client.update_certificate_status(
127+
page_id=existing_page_id,
128+
status=CertificateStatus.ISSUED,
129+
certificate_number=existing_cert_number,
130+
role=participation_info["user_role"]
131+
)
132+
133+
print(f"수료증 재발급 완료: {existing_cert_number}")
134+
135+
return CertificateResponse(
136+
status="200",
137+
message="기존 수료증이 재발급되었습니다.\n제출하신 이메일로 수료증이 발송되었습니다.",
138+
data=CertificateData(
139+
id=existing_page_id,
140+
name=certificate_data["applicant_name"],
141+
recipient_email=certificate_data["recipient_email"],
142+
certificate_number=existing_cert_number,
143+
issue_date=datetime.now().strftime("%Y-%m-%d"),
144+
certificate_status=CertificateStatus.ISSUED,
145+
season=certificate_data["season"],
146+
course_name=certificate_data["course_name"],
147+
role=Role.BUILDER if participation_info["user_role"] == "BUILDER" else Role.RUNNER
148+
)
149+
)
150+
151+
except Exception as e:
152+
print(f"수료증 재발급 중 오류: {e}")
153+
raise e
154+
155+
@staticmethod
156+
async def _create_new_certificate(
157+
certificate_data: dict,
158+
notion_client: NotionClient
159+
) -> CertificateResponse:
160+
"""신규 수료증 발급"""
161+
request_id = None
162+
try:
53163
# 수료증 요청 내역 생성
54164
certificate_request = await notion_client.create_certificate_request(certificate_data)
55165

@@ -121,17 +231,10 @@ async def create_certificate(certificate_data: dict) -> CertificateResponse:
121231

122232
except Exception as e:
123233
# 시스템 오류
124-
print(f"시스템 오류: {e}")
234+
print(f"신규 수료증 발급 중 시스템 오류: {e}")
125235
if request_id: # request_id가 존재하는 경우에만 상태 업데이트
126236
await notion_client.update_certificate_status(
127237
page_id=request_id,
128238
status=CertificateStatus.SYSTEM_ERROR
129239
)
130-
raise HTTPException(
131-
status_code=500,
132-
detail={
133-
"status": ResponseStatus.FAIL,
134-
"error_code": ErrorCodes.PIPELINE_ERROR,
135-
"message": f"{str(e)}"
136-
}
137-
)
240+
raise e

cert/backend/src/utils/notion_client.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime
22
import os
33
import aiohttp
44
from typing import Optional, Dict, Any, List
@@ -448,6 +448,110 @@ def sort_key(season_group: SeasonGroup) -> int:
448448
print(f"기수별 프로젝트 조회 중 오류: {e}")
449449
return None
450450

451+
async def check_existing_certificate(
452+
self,
453+
applicant_name: str,
454+
course_name: str,
455+
season: int,
456+
recipient_email: str = None
457+
) -> Optional[Dict[str, Any]]:
458+
"""기존 수료증 확인 (재발급용) - 이름, 코스명, 기수로 검색 (이메일 무관)"""
459+
try:
460+
async with aiohttp.ClientSession() as session:
461+
url = f"{self.base_url}/databases/{self.databases['certificate_requests']}/query"
462+
463+
# 필터 조건 구성 (이름, 코스명, 기수로만 검색 - 이메일은 무관)
464+
filters = [
465+
{
466+
"property": "Name",
467+
"title": {
468+
"equals": applicant_name
469+
}
470+
},
471+
{
472+
"property": "Course Name",
473+
"rich_text": {
474+
"equals": course_name
475+
}
476+
},
477+
{
478+
"property": "Season",
479+
"select": {
480+
"equals": f"{season}기"
481+
}
482+
}
483+
]
484+
485+
payload = {
486+
"filter": {
487+
"and": filters
488+
}
489+
}
490+
491+
async with session.post(url, headers=self.headers, json=payload) as response:
492+
if response.status == 200:
493+
data = await response.json()
494+
if data["results"]:
495+
# 가장 최근 수료증 반환 (첫 번째 결과)
496+
existing_cert = data["results"][0]
497+
properties = existing_cert.get("properties", {})
498+
499+
# 기존 수료증 정보 추출
500+
certificate_number = ""
501+
if "Certificate Number" in properties:
502+
cert_num_prop = properties["Certificate Number"].get("rich_text", [])
503+
if cert_num_prop:
504+
certificate_number = cert_num_prop[0].get("plain_text", "")
505+
506+
role = ""
507+
if "Role" in properties:
508+
role_prop = properties["Role"].get("select", {})
509+
if role_prop:
510+
role = role_prop.get("name", "")
511+
512+
status = ""
513+
if "Certificate Status" in properties:
514+
status_prop = properties["Certificate Status"].get("status", {})
515+
if status_prop:
516+
status = status_prop.get("name", "")
517+
518+
# 이메일 정보 추출
519+
existing_email = ""
520+
if "Recipient Email" in properties:
521+
email_prop = properties["Recipient Email"].get("email", "")
522+
if email_prop:
523+
existing_email = email_prop
524+
525+
print("🔍 기존 수료증 발견 (이름, 코스, 기수 일치):")
526+
print(f" - 이름: {applicant_name}")
527+
print(f" - 코스: {course_name}")
528+
print(f" - 기수: {season}기")
529+
print(f" - 수료증 번호: '{certificate_number}'")
530+
print(f" - 역할: '{role}'")
531+
print(f" - 상태: '{status}'")
532+
533+
return {
534+
"found": True,
535+
"page_id": existing_cert.get("id"),
536+
"certificate_number": certificate_number,
537+
"role": role,
538+
"status": status,
539+
"issue_date": properties.get("Issue Date", {}).get("date", {}).get("start", ""),
540+
"existing_email": existing_email,
541+
"existing_data": existing_cert
542+
}
543+
else:
544+
print(f"🔍 기존 수료증 없음: {applicant_name}, {course_name}, {season}기")
545+
return {"found": False}
546+
else:
547+
error_text = await response.text()
548+
print(f"기존 수료증 확인 오류: {response.status} - {error_text}")
549+
return None
550+
551+
except Exception as e:
552+
print(f"기존 수료증 확인 중 오류: {e}")
553+
return None
554+
451555
async def get_database_structure(self, database_type: str = "project_history") -> Optional[Dict[str, Any]]:
452556
"""데이터베이스 구조 조회 (디버깅용)"""
453557
try:

0 commit comments

Comments
 (0)