From c5bdd787a7765810a6b6ab8859e2035986d6436c Mon Sep 17 00:00:00 2001 From: hikeong Date: Tue, 8 Sep 2026 14:16:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B0=95=EC=97=B0=20=EC=8B=A0=EC=B2=AD?= =?UTF-8?q?=EC=84=9C=20=EC=A0=91=EC=88=98=20=EC=95=8C=EB=A6=BC=EC=9D=84=20?= =?UTF-8?q?=EC=97=AC=EB=9F=AC=20=EC=B1=84=EB=84=90=EB=A1=9C=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=20=EB=B0=9C=EC=86=A1=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/bot.py b/bot.py index 7c21596..5253d39 100644 --- a/bot.py +++ b/bot.py @@ -70,8 +70,11 @@ def _parse_channel_role_map(env_val: str) -> Dict[int, int]: 2: int(os.getenv("GRADE2_ROLE_ID", "1334466986419163187")), } -STUDENT_COUNCIL_CHANNEL_ID = int( - os.getenv("STUDENT_COUNCIL_CHANNEL_ID", "1542518682880839680") +SUBMISSION_NOTIFY_CHANNEL_IDS: List[int] = _parse_id_list( + os.getenv( + "SUBMISSION_NOTIFY_CHANNEL_IDS", + "1542518682880839680,1490575679786455111", + ) ) POLL_INTERVAL = int(os.getenv("POLL_INTERVAL", "30")) @@ -81,6 +84,7 @@ def _parse_channel_role_map(env_val: str) -> Dict[int, int]: EMBED_COLOR = 0xE8B84B FOOTER_TEXT = "GSM 릴스 봇" +# 신청 시작 시각 (한국 시간 기준 오후 4시 20분) KST = timezone(timedelta(hours=9)) OPEN_HOUR = int(os.getenv("OPEN_HOUR", "16")) OPEN_MINUTE = int(os.getenv("OPEN_MINUTE", "20")) @@ -147,6 +151,11 @@ def _make_progress_bar(enrolled: int, capacity: int, width: int = 10) -> str: def _compute_open_at_iso(approved_at_utc: datetime) -> str: + """강연 승인 시각(approved_at) 기준으로 신청 시작 시각을 계산해 UTC ISO 문자열로 반환한다. + + 규칙: 승인 시각이 그날 오후 4시 20분(KST) 이전이면 그날 4시 20분에 시작하고, + 이미 지났으면 다음날 4시 20분으로 넘어간다. + """ approved_kst = approved_at_utc.astimezone(KST) open_kst = approved_kst.replace( hour=OPEN_HOUR, minute=OPEN_MINUTE, second=0, microsecond=0 @@ -306,6 +315,7 @@ async def send_to_all_notify_channels( async def send_confirmed_notification( lecture: Dict[str, Any], message: str, embed: discord.Embed ) -> None: + """개설 확정 알림 전용 발송 함수. 멘션 없이 메시지만 보낸다.""" channel_ids = set(STATIC_NOTIFY_CHANNEL_ROLE_MAP) | set( GRADE_AWARE_NOTIFY_CHANNEL_IDS ) @@ -324,16 +334,18 @@ async def send_confirmed_notification( async def send_to_student_council(embed: discord.Embed) -> None: - channel = bot.get_channel(STUDENT_COUNCIL_CHANNEL_ID) - if channel: - try: - await channel.send(embed=embed) - except Exception as e: - print(f"[전송 에러] 학생회 채널로 메시지 전송 실패: {e}") - else: - print( - f"[채널 없음] {STUDENT_COUNCIL_CHANNEL_ID} — 봇이 이 채널을 못 찾음(권한/캐시 확인 필요)" - ) + """신청서 접수 알림 채널들로 멘션 없이 임베드만 전송한다.""" + for channel_id in SUBMISSION_NOTIFY_CHANNEL_IDS: + channel = bot.get_channel(channel_id) + if channel: + try: + await channel.send(embed=embed) + except Exception as e: + print(f"[전송 에러] 채널 {channel_id}로 메시지 전송 실패: {e}") + else: + print( + f"[채널 없음] {channel_id} — 봇이 이 채널을 못 찾음(권한/캐시 확인 필요)" + ) async def _process_due_open_notifications(lectures: List[Dict[str, Any]]) -> None: @@ -345,6 +357,7 @@ async def _process_due_open_notifications(lectures: List[Dict[str, Any]]) -> Non lectures_by_id = {str(lec["id"]): lec for lec in lectures} for due in due_list: + # 먼저 notified 처리해서, 강연을 못 찾아도 다음 폴링에서 또 시도하지 않도록 한다. mark_open_notified(due["lecture_id"]) lecture = lectures_by_id.get(due["lecture_id"]) @@ -445,6 +458,9 @@ async def before_poll() -> None: if lecture.get("status") == "OPEN": mark_notified(lecture_id, "new", lecture["title"]) + # 봇 재시작 시점에 이미 존재하던 강연은 신청 시작 알림 대상에서 제외한다. + # (주의: DB가 초기화된 상태로 재시작되면, 실제로 안 나갔던 알림도 + # 이미 보낸 것으로 처리될 수 있음 - 근본 해결은 DB 영구 저장 설정) schedule_open_notification( lecture_id, now_iso, lecture["title"], notified=1 )