|
1 | 1 | import logging |
2 | 2 | import uuid |
| 3 | +import re |
3 | 4 | from datetime import datetime |
4 | 5 | from typing import Optional, List |
5 | 6 |
|
@@ -35,6 +36,64 @@ def clear_cache(): |
35 | 36 |
|
36 | 37 | class CertificateService: |
37 | 38 | """์๋ฃ์ฆ ์๋น์ค""" |
| 39 | + |
| 40 | + @staticmethod |
| 41 | + def _get_study_year(period: dict) -> int: |
| 42 | + """์คํฐ๋ ๊ธฐ๊ฐ์์ ์ฐ๋๋ฅผ ์ถ์ถ (์์ผ๋ฉด ํ์ฌ ์ฐ๋ fallback)""" |
| 43 | + for key in ("start", "end"): |
| 44 | + raw_date = (period or {}).get(key) |
| 45 | + if raw_date: |
| 46 | + try: |
| 47 | + return datetime.fromisoformat(raw_date).year |
| 48 | + except ValueError: |
| 49 | + # ISO ํฌ๋งท์ด ์๋๋ฉด ์ฐ๋๋ง ํ์ฑ ์๋ |
| 50 | + year_part = raw_date.split("-", 1)[0] |
| 51 | + if year_part.isdigit(): |
| 52 | + return int(year_part) |
| 53 | + return datetime.now().year |
| 54 | + |
| 55 | + @staticmethod |
| 56 | + def _build_certificate_number( |
| 57 | + study_year: int, |
| 58 | + unique_identifier: str, |
| 59 | + track_code: str, |
| 60 | + season_code: str, |
| 61 | + study_id: str, |
| 62 | + ) -> str: |
| 63 | + """์๋ฃ์ฆ ๋ฒํธ ์์ฑ: {์ฝ๋}{์ฐ๋}{๊ธฐ์}-{์คํฐ๋ID}{unique_id}""" |
| 64 | + suffix = f"{study_id}{unique_identifier.upper()}" |
| 65 | + return f"{track_code}{study_year}{season_code}-{suffix}" |
| 66 | + |
| 67 | + @staticmethod |
| 68 | + def _parse_project_code(project_code: str, season: int) -> tuple[str | None, str, str | None]: |
| 69 | + """ํ๋ก์ ํธ ์ฝ๋ ํ์ฑ: CODE์์ ํธ๋/๊ธฐ์/์คํฐ๋ID ์ถ์ถ""" |
| 70 | + track_code = None |
| 71 | + season_code = f"S{season:02d}" |
| 72 | + study_id = None |
| 73 | + |
| 74 | + if not project_code: |
| 75 | + return track_code, season_code, study_id |
| 76 | + |
| 77 | + raw_code = project_code.strip() |
| 78 | + if "-" in raw_code: |
| 79 | + parts = raw_code.split("-", 1) |
| 80 | + elif "_" in raw_code: |
| 81 | + parts = raw_code.split("_", 1) |
| 82 | + else: |
| 83 | + parts = [raw_code] |
| 84 | + |
| 85 | + if parts: |
| 86 | + season_match = re.match(r"^S(\d+)$", parts[0], re.IGNORECASE) |
| 87 | + if season_match: |
| 88 | + season_code = f"S{int(season_match.group(1)):02d}" |
| 89 | + |
| 90 | + target = parts[1] if len(parts) > 1 else raw_code |
| 91 | + match = re.match(r"^([A-Za-z]+)(\d+)$", target) |
| 92 | + if match: |
| 93 | + track_code = match.group(1).upper() |
| 94 | + study_id = match.group(2) |
| 95 | + |
| 96 | + return track_code, season_code, study_id |
38 | 97 |
|
39 | 98 | @staticmethod |
40 | 99 | async def create_certificate(certificate_data: dict) -> CertificateResponse: |
@@ -133,7 +192,7 @@ async def verify_certificate(file_bytes: bytes) -> dict: |
133 | 192 | "debug_text": watermark_text |
134 | 193 | } |
135 | 194 |
|
136 | | - # ์๋ฃ์ฆ ๋ฒํธ ์ถ์ถ (CERT-2026XX ํฌ๋งท ๊ธฐ๋) |
| 195 | + # ์๋ฃ์ฆ ๋ฒํธ ์ถ์ถ (A2025S10-0156 ํฌ๋งท ๊ธฐ๋) |
137 | 196 | cert_number = "" |
138 | 197 | if "_" in watermark_text: |
139 | 198 | cert_number = watermark_text.split("_")[1] |
@@ -264,7 +323,38 @@ async def _reissue_certificate( |
264 | 323 | break |
265 | 324 |
|
266 | 325 |
|
267 | | - existing_cert_number = f"CERT-{datetime.now().year}{participation_info['project_code']}{unique_identifier.upper()}" |
| 326 | + study_year = CertificateService._get_study_year(participation_info.get("period", {})) |
| 327 | + track_code, season_code, study_id = CertificateService._parse_project_code( |
| 328 | + participation_info.get("project_code", ""), |
| 329 | + certificate_data["season"], |
| 330 | + ) |
| 331 | + if not track_code: |
| 332 | + track_code = "N" |
| 333 | + if not study_id: |
| 334 | + study_index = await notion_client.get_study_order_index( |
| 335 | + season=certificate_data["season"], |
| 336 | + course_name=certificate_data["course_name"], |
| 337 | + ) |
| 338 | + if study_index is None: |
| 339 | + message = "์คํฐ๋ ์์๋ฅผ ํ์ธํ ์ ์์ด ์๋ฃ์ฆ์ ๋ฐ๊ธํ ์ ์์ต๋๋ค." |
| 340 | + logger.error( |
| 341 | + message, |
| 342 | + extra={ |
| 343 | + "season": certificate_data["season"], |
| 344 | + "course_name": certificate_data["course_name"], |
| 345 | + }, |
| 346 | + ) |
| 347 | + raise Exception(message) |
| 348 | + else: |
| 349 | + study_id = f"{study_index:02d}" if study_index < 100 else str(study_index) |
| 350 | + |
| 351 | + existing_cert_number = CertificateService._build_certificate_number( |
| 352 | + study_year=study_year, |
| 353 | + unique_identifier=unique_identifier, |
| 354 | + track_code=track_code, |
| 355 | + season_code=season_code, |
| 356 | + study_id=study_id, |
| 357 | + ) |
268 | 358 | logger.info( |
269 | 359 | "์๋ก์ด ์๋ฃ์ฆ ๋ฒํธ ์์ฑ", |
270 | 360 | extra={"certificate_number": existing_cert_number}, |
@@ -398,7 +488,38 @@ async def _create_new_certificate( |
398 | 488 | # fallback: Page ID์ ๋ง์ง๋ง 5์๋ฆฌ |
399 | 489 | unique_identifier = request_id.replace("-", "")[-5:] |
400 | 490 |
|
401 | | - certificate_number = f"CERT-{datetime.now().year}{participation_info['project_code']}{unique_identifier.upper()}" |
| 491 | + study_year = CertificateService._get_study_year(participation_info.get("period", {})) |
| 492 | + track_code, season_code, study_id = CertificateService._parse_project_code( |
| 493 | + participation_info.get("project_code", ""), |
| 494 | + certificate_data["season"], |
| 495 | + ) |
| 496 | + if not track_code: |
| 497 | + track_code = "N" |
| 498 | + if not study_id: |
| 499 | + study_index = await notion_client.get_study_order_index( |
| 500 | + season=certificate_data["season"], |
| 501 | + course_name=certificate_data["course_name"], |
| 502 | + ) |
| 503 | + if study_index is None: |
| 504 | + message = "์คํฐ๋ ์์๋ฅผ ํ์ธํ ์ ์์ด ์๋ฃ์ฆ์ ๋ฐ๊ธํ ์ ์์ต๋๋ค." |
| 505 | + logger.error( |
| 506 | + message, |
| 507 | + extra={ |
| 508 | + "season": certificate_data["season"], |
| 509 | + "course_name": certificate_data["course_name"], |
| 510 | + }, |
| 511 | + ) |
| 512 | + raise Exception(message) |
| 513 | + else: |
| 514 | + study_id = f"{study_index:02d}" if study_index < 100 else str(study_index) |
| 515 | + |
| 516 | + certificate_number = CertificateService._build_certificate_number( |
| 517 | + study_year=study_year, |
| 518 | + unique_identifier=unique_identifier, |
| 519 | + track_code=track_code, |
| 520 | + season_code=season_code, |
| 521 | + study_id=study_id, |
| 522 | + ) |
402 | 523 | issue_date = datetime.now().strftime("%Y-%m-%d") |
403 | 524 |
|
404 | 525 | # PDF ์๋ฃ์ฆ ์์ฑ |
|
0 commit comments