From 1e823e2877c7544697b2a1485d44f5d0094d3e6c Mon Sep 17 00:00:00 2001 From: choihooo Date: Tue, 10 Mar 2026 15:50:16 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20RSS=20=EC=88=98=EC=A7=91=20?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=9C=EC=84=9D=20=EC=83=81=ED=83=9C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B0=8F=20=EA=B2=B0=EC=84=9D/?= =?UTF-8?q?=EC=A7=80=EA=B0=81=20=EB=B2=8C=EA=B8=88=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EB=B6=80=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 #3 문제 해결 - 출석 상태 업데이트 누락: - RSS 콜백에서 새 글 생성 후 출석 상태 업데이트 - 회차 마감일 이후 제출 시 지각(LATE) 처리 - 지각 시 벌금 3,000원 자동 부과 및 DM 발송 - 정상 제출 시 SUBMITTED 상태로 변경 P0 #4 문제 해결 - 결석/지각 벌금 콜백 미설정: - attendanceChecker에 결석 콜백 설정 - 화요일 00:00 결석 판정 시 자동으로 벌금 5,000원 부과 - 결석 벌금 DM 알림 자동 발송 변경 사항: - scheduler-registry.ts: RSS 콜백에 출석 상태 업데이트 로직 추가 - 지각 판단: item.pubDate > round.endDate (23:59:59.999) - 결석 콜백: setOnAbsentCallback()으로 벌금 생성 및 알림 발송 동작 흐름: 1. RSS 폴러가 새 글 발견 2. 글 저장 + 출석 상태 업데이트 (SUBMITTED 또는 LATE) 3. 지각인 경우 벌금 생성 + DM 발송 4. 화요일 00:00 출석 체크 5. 결석자 벌금 생성 + DM 발송 Co-Authored-By: Claude Sonnet 4.6 --- packages/bot/src/scheduler-registry.ts | 73 +++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/bot/src/scheduler-registry.ts b/packages/bot/src/scheduler-registry.ts index 136288f..307964b 100644 --- a/packages/bot/src/scheduler-registry.ts +++ b/packages/bot/src/scheduler-registry.ts @@ -17,7 +17,9 @@ import type { CrawledContent } from './services/curation.service'; import { getPostService } from './services/post.service'; import { getNotificationService } from './services/notification.service'; import { getScoreService } from './services/score.service'; -import { ActivityScoreType, curationSources, getDb } from '@blog-study/shared/db'; +import { getAttendanceService, getFineService } from './services'; +import { sendFineNotification } from './handlers/dm-handler'; +import { getDb, members, ActivityScoreType, curationSources } from '@blog-study/shared/db'; import { getCurrentRound } from './services/round.service'; import { eq } from 'drizzle-orm'; @@ -54,10 +56,12 @@ export async function registerAllJobs(boss: PgBoss, client: Client): Promise roundEndDate; + + if (isLate) { + // 지각: 출석 상태 업데이트 + 벌금 부과 + await attendanceService.markLate(member.id, currentRound.id); + console.log(`⏰ ${member.name} 지각 처리 (${currentRound.roundNumber}회차)`); + + // 지각 벌금 생성 + const fine = await fineService.create(member.id, currentRound.id, 'late'); + await sendFineNotification( + client, + member.discordId, + fine.id, + fine.amount, + 'late', + currentRound.roundNumber + ); + } else { + // 정상 제출 + await attendanceService.markSubmitted(member.id, currentRound.id); + console.log(`✅ ${member.name} 제출 완료 (${currentRound.roundNumber}회차)`); + } + } + // 블로그 포스트 점수 부여 (+30점, 일일 2편 상한) const safeTitle = item.title.replace(/[<>"'&]/g, '').slice(0, 200); await scoreService.grantScore( @@ -95,6 +129,41 @@ export async function registerAllJobs(boss: PgBoss, client: Client): Promise { + try { + const db = getDb(); + const [member] = await db + .select() + .from(members) + .where(eq(members.id, attendance.memberId)) + .limit(1); + + if (!member) { + console.error(`❌ Member not found: ${attendance.memberId}`); + return; + } + + // 결석 벌금 생성 + const fine = await fineService.create(attendance.memberId, round.id, 'absent'); + + // DM으로 벌금 알림 발송 + await sendFineNotification( + client, + member.discordId, + fine.id, + fine.amount, + 'absent', + round.roundNumber + ); + + console.log(`❌ ${member.name} 결석 벌금 부과 (${round.roundNumber}회차, ${fine.amount}원)`); + } catch (error) { + console.error(`❌ Failed to process absent callback for ${attendance.memberId}:`, error); + } + }); + // Set up curation crawl function: fetch RSS → parse → return CrawledContent[] curationCrawler.setCrawlFunction(async (url: string): Promise => { // Look up the source's rssUrl from DB (source.url might differ from RSS URL) From c3136124520508ec201fa89be412d3ff4371153b Mon Sep 17 00:00:00 2001 From: choihooo Date: Tue, 10 Mar 2026 16:46:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=20(?= =?UTF-8?q?=ED=83=80=EC=9E=84=EC=A1=B4,=20=EC=A3=BC=EC=84=9D,=20import=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Priority 1 & 2 수정 사항: 1. 타임존 명시: roundEndDate 생성 시 KST (+09:00) 명시 2. 중복 방지 로직 주석 추가: markLate/markSubmitted/fineService.create 내부 로직 설명 3. Import 순서 정리: @blog-study/shared/db를 상단으로 이동 기존 로직 확인: - FineService.create(): 이미 내부에서 getByMemberAndRound() 체크 후 중복 시 기존 벌금 반환 - AttendanceService.markLate/markSubmitted(): 이미 PENDING 상태일 때만 업데이트 Co-Authored-By: Claude Sonnet 4.6 --- packages/bot/src/scheduler-registry.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/bot/src/scheduler-registry.ts b/packages/bot/src/scheduler-registry.ts index 307964b..0b00022 100644 --- a/packages/bot/src/scheduler-registry.ts +++ b/packages/bot/src/scheduler-registry.ts @@ -85,17 +85,20 @@ export async function registerAllJobs(boss: PgBoss, client: Client): Promise roundEndDate; if (isLate) { // 지각: 출석 상태 업데이트 + 벌금 부과 + // markLate()와 fineService.create()는 내부에서 중복 방지 로직을 가짐: + // - markLate(): PENDING 상태일 때만 LATE로 변경 (기존 LATE/ABSENT 유지) + // - fineService.create(): 동일 회차 벌금이 이미 있으면 기존 벌금 반환 await attendanceService.markLate(member.id, currentRound.id); console.log(`⏰ ${member.name} 지각 처리 (${currentRound.roundNumber}회차)`); - // 지각 벌금 생성 + // 지각 벌금 생성 (이미 존재하면 기존 벌금 반환) const fine = await fineService.create(member.id, currentRound.id, 'late'); await sendFineNotification( client, @@ -107,6 +110,7 @@ export async function registerAllJobs(boss: PgBoss, client: Client): Promise