diff --git a/packages/bot/src/scheduler-registry.ts b/packages/bot/src/scheduler-registry.ts index 136288f..0b00022 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) { + // 지각: 출석 상태 업데이트 + 벌금 부과 + // 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, + member.discordId, + fine.id, + fine.amount, + 'late', + currentRound.roundNumber + ); + } else { + // 정상 제출 + // markSubmitted()는 내부에서 PENDING 상태일 때만 SUBMITTED로 변경 (기존 LATE/ABSENT 유지) + 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 +133,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)