Skip to content

Commit 804bc4a

Browse files
committed
feat(cert): add certificate authenticity check
1 parent 4b556c5 commit 804bc4a

File tree

5 files changed

+481
-163
lines changed

5 files changed

+481
-163
lines changed

cert/backend/src/models/certificate.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class CertificateData(BaseModel):
2828
id: str = Field(..., example="2533a2a2-eed5-81fa-9921-c14d2cd117b7", description="수료증 신청 페이지 ID")
2929
name: str = Field(..., example="홍길동", description="신청자 이름")
3030
recipient_email: str = Field(..., example="hong@example.com", description="수료자 이메일")
31-
certificate_number: str = Field(..., example="CERT-2024-001", description="수료증 번호")
31+
certificate_number: str = Field(..., example="CERT-2026DC", description="수료증 번호")
3232
issue_date: str = Field(..., example="2024-01-15", description="신청 날짜")
3333
certificate_status: CertificateStatus = Field(..., example=CertificateStatus.PENDING, description="발급 여부")
3434
season: int = Field(..., example=10, description="참여 기수")
@@ -42,6 +42,26 @@ class CertificateResponse(BaseModel):
4242
data: Optional[CertificateData] = Field(None, description="수료증 데이터")
4343

4444

45+
class CertificateVerifyRequest(BaseModel):
46+
"""수료증 번호 확인 요청 모델"""
47+
certificate_number: str = Field(..., example="CERT-2026DC", description="수료증 번호")
48+
49+
50+
class CertificateVerifyData(BaseModel):
51+
"""수료증 번호 확인 데이터"""
52+
name: str = Field(..., example="홍길동", description="신청자 이름")
53+
course: str = Field(..., example="Wrapping Up Pseudolab", description="스터디명")
54+
season: str = Field(..., example="10기", description="참여 기수")
55+
issue_date: str = Field(..., example="2024-01-15", description="발급일")
56+
57+
58+
class CertificateVerifyResponse(BaseModel):
59+
"""수료증 번호 확인 응답"""
60+
valid: bool = Field(..., example=True, description="확인 여부")
61+
message: str = Field(..., example="수료증 확인에 성공했습니다.", description="결과 메시지")
62+
data: Optional[CertificateVerifyData] = Field(None, description="수료증 정보")
63+
64+
4565
class ErrorResponse(BaseModel):
4666
"""에러 응답 모델"""
4767
status: str = Field(..., example="fail", description="응답 상태")

cert/backend/src/routers/certificate.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from fastapi import APIRouter, HTTPException, Response, File, UploadFile
22

33
from ..models.project import Project, ProjectsBySeasonResponse
4-
from ..models.certificate import CertificateCreate, CertificateResponse, ErrorResponse
4+
from ..models.certificate import (
5+
CertificateCreate,
6+
CertificateResponse,
7+
CertificateVerifyRequest,
8+
CertificateVerifyResponse,
9+
ErrorResponse,
10+
)
511
from ..services.certificate_service import CertificateService, ProjectService
612
from ..constants.error_codes import ResponseStatus
713

@@ -107,3 +113,30 @@ async def verify_certificate(file: UploadFile = File(...)):
107113
import logging
108114
logging.error(f"검증 중 오류 발생: {e}")
109115
raise HTTPException(status_code=500, detail="파일 처리 중 오류가 발생했습니다.")
116+
117+
@certificate_router.post(
118+
"/verify-by-number",
119+
response_model=CertificateVerifyResponse,
120+
responses={
121+
200: {
122+
"description": "수료증 번호 확인 성공/실패",
123+
"model": CertificateVerifyResponse
124+
},
125+
400: {
126+
"description": "잘못된 요청",
127+
"model": ErrorResponse
128+
},
129+
500: {
130+
"description": "서버 내부 오류",
131+
"model": ErrorResponse
132+
}
133+
}
134+
)
135+
async def verify_certificate_by_number(payload: CertificateVerifyRequest):
136+
"""수료증 번호로 수료 여부를 확인합니다."""
137+
certificate_number = payload.certificate_number.strip()
138+
if not certificate_number:
139+
raise HTTPException(status_code=400, detail="수료증 번호를 입력해주세요.")
140+
141+
result = await CertificateService.verify_certificate_by_number(certificate_number)
142+
return result

cert/backend/src/services/certificate_service.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ async def verify_certificate(file_bytes: bytes) -> dict:
133133
"debug_text": watermark_text
134134
}
135135

136-
# 수료증 번호 추출 (PSEUDOLAB_CERT-XXXX 포맷 기대)
136+
# 수료증 번호 추출 (CERT-2026XX 포맷 기대)
137137
cert_number = ""
138138
if "_" in watermark_text:
139139
cert_number = watermark_text.split("_")[1]
@@ -155,28 +155,7 @@ async def verify_certificate(file_bytes: bytes) -> dict:
155155
"valid": False,
156156
"message": f"수료증 번호({cert_number})에 해당하는 발급 기록을 찾을 수 없습니다."
157157
}
158-
159-
# Notion 결과 파싱
160-
props = cert_page.get("properties", {})
161-
162-
name = props.get("Name", {}).get("title", [{}])[0].get("plain_text", "알 수 없음")
163-
course = props.get("Course Name", {}).get("rich_text", [{}])[0].get("plain_text", "알 수 없음")
164-
season = props.get("Season", {}).get("select", {}).get("name", "알 수 없음")
165-
issue_date = props.get("Issue Date", {}).get("date", {}).get("start", "알 수 없음")
166-
status = props.get("Certificate Status", {}).get("status", {}).get("name", "알 수 없음")
167-
168-
return {
169-
"valid": True,
170-
"message": "수료증 진위 확인에 성공했습니다.",
171-
"data": {
172-
"name": name,
173-
"course": course,
174-
"season": season,
175-
"issue_date": issue_date,
176-
"certificate_number": cert_number,
177-
"status": status
178-
}
179-
}
158+
return CertificateService._build_verification_result(cert_page, cert_number)
180159

181160
except Exception as e:
182161
if "워터마크를 찾을 수 없습니다" in str(e):
@@ -187,6 +166,49 @@ async def verify_certificate(file_bytes: bytes) -> dict:
187166
"valid": False,
188167
"message": "수료증 검증 처리 중 오류가 발생했습니다."
189168
}
169+
170+
@staticmethod
171+
async def verify_certificate_by_number(certificate_number: str) -> dict:
172+
"""수료증 번호로 수료 여부 확인"""
173+
try:
174+
notion_client = NotionClient()
175+
cert_page = await notion_client.get_certificate_by_number(certificate_number)
176+
177+
if not cert_page:
178+
return {
179+
"valid": False,
180+
"message": f"수료증 번호({certificate_number})에 해당하는 발급 기록을 찾을 수 없습니다."
181+
}
182+
183+
return CertificateService._build_verification_result(cert_page, certificate_number)
184+
except Exception:
185+
logger.exception("수료증 번호 확인 중 오류")
186+
return {
187+
"valid": False,
188+
"message": "수료증 번호 확인 처리 중 오류가 발생했습니다."
189+
}
190+
191+
@staticmethod
192+
def _build_verification_result(cert_page: dict, certificate_number: str) -> dict:
193+
"""Notion 수료증 페이지 응답 포맷팅"""
194+
props = cert_page.get("properties", {})
195+
196+
name = props.get("Name", {}).get("title", [{}])[0].get("plain_text", "알 수 없음")
197+
course = props.get("Course Name", {}).get("rich_text", [{}])[0].get("plain_text", "알 수 없음")
198+
season = props.get("Season", {}).get("select", {}).get("name", "알 수 없음")
199+
issue_date = props.get("Issue Date", {}).get("date", {}).get("start", "알 수 없음")
200+
status = props.get("Certificate Status", {}).get("status", {}).get("name", "알 수 없음")
201+
202+
return {
203+
"valid": True,
204+
"message": "수료증 확인에 성공했습니다.",
205+
"data": {
206+
"name": name,
207+
"course": course,
208+
"season": season,
209+
"issue_date": issue_date
210+
}
211+
}
190212

191213
@staticmethod
192214
async def _reissue_certificate(

cert/docs/api.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,61 @@ URL : https://cert.pseudolab-devfactory/api/certs/create
101101
"message": "발급 처리 중 오류가 발생했습니다."
102102
}
103103
}
104-
```
104+
```
105+
106+
---
107+
108+
## 수료증 번호 확인
109+
수료증 번호로 여부를 확인합니다.
110+
111+
HTTP Method : POST
112+
URL : https://cert.pseudolab-devfactory/api/certs/verify-by-number
113+
114+
### Request
115+
#### Header
116+
| 이름 | 내용 | 필수 |
117+
|--------|----------------|:--:|
118+
| Content-Type | `application/json` | O |
119+
120+
#### Body
121+
| 이름 | 타입 | 설명 | 필수 |
122+
|---|---|---|:--:|
123+
| certificate_number | String | 수료증 번호 | O |
124+
125+
### Response
126+
#### 성공/실패 공통
127+
| 이름 | 타입 | 설명 |
128+
|---|---|---|
129+
| valid | Boolean | 수료 여부 |
130+
| message | String | 결과 메세지 |
131+
| data | Object | 수료증 상세 (valid=true일 때) |
132+
133+
### Example
134+
#### Request
135+
```json
136+
{
137+
"certificate_number": "CERT-202418"
138+
}
139+
```
140+
141+
#### 성공 응답
142+
```json
143+
{
144+
"valid": true,
145+
"message": "수료증 확인에 성공했습니다.",
146+
"data": {
147+
"name": "홍길동",
148+
"course": "Wrapping Up Pseudolab",
149+
"season": "10기",
150+
"issue_date": "2024-01-15"
151+
}
152+
}
153+
```
154+
155+
#### 실패 응답 (발급 기록 없음)
156+
```json
157+
{
158+
"valid": false,
159+
"message": "수료증 번호(CERT-202618)에 해당하는 발급 기록을 찾을 수 없습니다."
160+
}
161+
```

0 commit comments

Comments
 (0)