From 07f16683bc794222965603c279ed00ae7190a451 Mon Sep 17 00:00:00 2001 From: bbbang105 <2018111366@dgu.ac.kr> Date: Mon, 6 Apr 2026 11:10:13 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=EC=B6=9C=EC=84=9D=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A4=84=EB=9F=AC=20=EB=B6=84=EB=A6=AC=20+=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=EC=9D=B4=EC=A0=84=20=ED=9A=8C?= =?UTF-8?q?=EC=B0=A8=20=EA=B8=B0=EC=A4=80=20+=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=AC=B8=EA=B5=AC=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=EB=B6=80=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - attendance-init (월 00:02): 회차 전환 + active 멤버 PENDING 생성 - attendance-absent (화 00:02): 이전 회차 결석 처리 + 벌금 부과 - round-start/round-report는 Discord 알림만 담당 (DB 로직 분리) - round-report: 이전 회차 기준으로 발송, 지각/결석 명단 제거 - 대시보드 마감 압박 문구: active + 미제출 유저만 표시 Co-Authored-By: Claude --- packages/bot/src/api-server.ts | 27 +++-- packages/bot/src/api/types.ts | 2 - packages/bot/src/scheduler-registry.ts | 110 +++++++++++------- packages/bot/src/schedulers/index.ts | 1 - packages/bot/src/schedulers/round-reporter.ts | 76 +++++------- .../bot/src/services/notification.service.ts | 26 +---- .../web/src/app/(user)/dashboard/page.tsx | 18 ++- 7 files changed, 128 insertions(+), 132 deletions(-) diff --git a/packages/bot/src/api-server.ts b/packages/bot/src/api-server.ts index dccc080..f772ab6 100644 --- a/packages/bot/src/api-server.ts +++ b/packages/bot/src/api-server.ts @@ -8,7 +8,6 @@ import rateLimit from 'express-rate-limit'; import logger from './lib/logger'; import { Sentry } from './lib/sentry'; import { - getAttendanceChecker, getCurationCrawler, getDeadlineReminder, getFineReminder, @@ -17,6 +16,10 @@ import { getRssPoller, getWeeklyRanking, } from './schedulers'; +import { getAttendanceService } from './services/attendance.service'; +import { getFineService } from './services'; +import { getCurrentRound } from './services/round.service'; +import { AttendanceStatus } from '@blog-study/shared/db'; const BOT_API_SECRET = process.env.BOT_API_SECRET; @@ -79,14 +82,24 @@ export function createBotApiServer(): Express { app.post('/api/trigger/attendance-check', authMiddleware, triggerLimiter, async (_req, res) => { try { - const attendanceChecker = getAttendanceChecker(); - - if (attendanceChecker.isChecking()) { - return res.status(409).json({ error: '출석 체크가 이미 실행 중입니다' }); + const currentRound = await getCurrentRound(); + const attendanceService = getAttendanceService(); + const fineService = getFineService(); + + // PENDING → ABSENT 처리 + const processedRecords = await attendanceService.processGracePeriodEnd(currentRound.id); + const absentRecords = processedRecords.filter(r => r.status === AttendanceStatus.ABSENT); + + // 결석 벌금 부과 + for (const record of absentRecords) { + try { + await fineService.create(record.memberId, currentRound.id, 'absent'); + } catch (fineError) { + logger.error({ memberId: record.memberId, error: fineError }, '🌐 [API] 결석 벌금 부과 실패'); + } } - const result = await attendanceChecker.check(); - res.json({ success: true, result }); + res.json({ success: true, result: { processedCount: absentRecords.length } }); } catch (error) { Sentry.captureException(error); logger.error({ error }, '🌐 [API] 출석 체크 에러'); diff --git a/packages/bot/src/api/types.ts b/packages/bot/src/api/types.ts index d6db256..28efc4d 100644 --- a/packages/bot/src/api/types.ts +++ b/packages/bot/src/api/types.ts @@ -5,7 +5,6 @@ import type { PollingCycleResult, - AttendanceCheckResult, FineReminderResult, RoundReportResult, CurationCycleResult, @@ -44,7 +43,6 @@ export interface OperationInfo { */ export type OperationResult = | PollingCycleResult - | AttendanceCheckResult | FineReminderResult | RoundReportResult | CurationCycleResult diff --git a/packages/bot/src/scheduler-registry.ts b/packages/bot/src/scheduler-registry.ts index a6f18c6..e4d5bfa 100644 --- a/packages/bot/src/scheduler-registry.ts +++ b/packages/bot/src/scheduler-registry.ts @@ -8,7 +8,6 @@ import type { Client } from 'discord.js'; import axios from 'axios'; import { parseFeed } from 'feedsmith'; import { getRssPoller } from './schedulers/rss-poller'; -import { getAttendanceChecker } from './schedulers/attendance-checker'; import { getFineReminder } from './schedulers/fine-reminder'; import { getRoundReporter } from './schedulers/round-reporter'; import { getCurationCrawler } from './schedulers/curation-crawler'; @@ -20,12 +19,12 @@ import { getNotificationService } from './services/notification.service'; import { getScoreService } from './services/score.service'; import { getAttendanceService, getFineService } from './services'; -import { ActivityScoreType, curationSources, getDb, members } from '@blog-study/shared/db'; -import { extractFirstImage, extractOgImage } from '@blog-study/shared/utils'; -import { getCurrentRound } from './services/round.service'; +import { ActivityScoreType, AttendanceStatus, curationSources, getDb } from '@blog-study/shared/db'; +import { extractFirstImage, extractOgImage, formatKSTDate } from '@blog-study/shared/utils'; +import { getCurrentRound, getRoundByNumber, isGracePeriodEnded, setCurrentRound } from './services/round.service'; +import { Sentry } from './lib/sentry'; import { eq } from 'drizzle-orm'; import logger from './lib/logger'; -import { Sentry } from './lib/sentry'; /** * Job definitions with cron schedules @@ -33,7 +32,8 @@ import { Sentry } from './lib/sentry'; // pg-boss cron은 UTC 기준. KST = UTC+9 const JOB_DEFINITIONS = [ { name: 'rss-poll', cron: '*/5 * * * *' }, // 5분마다 - { name: 'attendance-check', cron: '0 0 * * 2' }, // KST 화 09:00 (UTC 화 00:00) + { name: 'attendance-init', cron: '2 15 * * 0' }, // KST 월 00:02 (UTC 일 15:02) — 회차 시작일 출석 PENDING 생성 + { name: 'attendance-absent', cron: '2 15 * * 1' }, // KST 화 00:02 (UTC 월 15:02) — PENDING → ABSENT + 벌금 { name: 'fine-reminder', cron: '0 0 * * *' }, // KST 매일 09:00 (UTC 00:00) { name: 'round-report', cron: '0 23 * * 1' }, // KST 화 08:00 (UTC 월 23:00) { name: 'round-start', cron: '0 23 * * 0' }, // KST 월 08:00 (UTC 일 23:00) @@ -51,7 +51,6 @@ const JOB_DEFINITIONS = [ export async function registerAllJobs(boss: PgBoss, client: Client): Promise { // Initialize scheduler instances with Discord client const rssPoller = getRssPoller(); - const attendanceChecker = getAttendanceChecker(); const fineReminder = getFineReminder(); const roundReporter = getRoundReporter(); const curationCrawler = getCurationCrawler(); @@ -181,40 +180,6 @@ 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) { - logger.error({ memberId: attendance.memberId }, '✅ [출석] 멤버를 찾을 수 없음'); - return; - } - - // 결석 벌금 생성 - // DM 알림은 보내지 않음 — fine-reminder에서 화요일부터 리마인더로 발송 - const fine = await fineService.create(attendance.memberId, round.id, 'absent'); - - logger.info({ - member: member.name, - round: round.roundNumber, - amount: fine.amount, - }, '✅ [출석] 결석 벌금 부과 완료'); - } catch (error) { - Sentry.captureException(error); - logger.error({ - memberId: attendance.memberId, - error - }, '✅ [출석] 결석 콜백 처리 실패'); - } - }); - // Set up curation crawl function: fetch RSS → parse → extract content → return CrawledContent[] // P1 #8: 큐레이션 데이터 품질 개선 - description, thumbnailUrl 추출 curationCrawler.setCrawlFunction(async (url: string): Promise => { @@ -276,8 +241,67 @@ export async function registerAllJobs(boss: PgBoss, client: Client): Promise { - await attendanceChecker.check(); + // 새 큐 생성 (기존에 없는 큐는 명시적으로 생성 필요) + await boss.createQueue('attendance-init'); + await boss.createQueue('attendance-absent'); + + // 회차 시작일 00:02 KST — 다음 회차 전환 + active 멤버 출석 PENDING 레코드 생성 + await boss.work('attendance-init', { batchSize: 1 }, async () => { + try { + const currentRound = await getCurrentRound(); + const todayStr = formatKSTDate(new Date()); + const nextRound = await getRoundByNumber(currentRound.roundNumber + 1); + + if (!nextRound || todayStr !== nextRound.startDate) { + logger.info(`✅ [출석 초기화] 오늘(${todayStr})은 다음 회차 시작일이 아님, 스킵`); + return; + } + + // 회차 전환 + await setCurrentRound(nextRound.roundNumber); + logger.info(`✅ [출석 초기화] ${nextRound.roundNumber}회차로 전환 완료`); + + // 출석 PENDING 레코드 생성 + const created = await attendanceService.createForRound(nextRound.id); + logger.info(`✅ [출석 초기화] ${nextRound.roundNumber}회차 ${created.length}명 PENDING 레코드 생성`); + } catch (error) { + Sentry.captureException(error); + logger.error({ error }, '✅ [출석 초기화] 에러'); + } + }); + + // 화요일 00:02 KST — 이전 회차 PENDING → ABSENT + 결석 벌금 부과 + await boss.work('attendance-absent', { batchSize: 1 }, async () => { + try { + const currentRound = await getCurrentRound(); + const prevRound = await getRoundByNumber(currentRound.roundNumber - 1); + + if (!prevRound) { + logger.info('✅ [결석 처리] 이전 회차 없음, 스킵'); + return; + } + + if (!isGracePeriodEnded(prevRound)) { + logger.info(`✅ [결석 처리] ${prevRound.roundNumber}회차 유예 기간 미종료, 스킵`); + return; + } + + const processedRecords = await attendanceService.processGracePeriodEnd(prevRound.id); + const absentRecords = processedRecords.filter(r => r.status === AttendanceStatus.ABSENT); + logger.info(`✅ [결석 처리] ${prevRound.roundNumber}회차 ${absentRecords.length}명 결석 처리`); + + for (const record of absentRecords) { + try { + await fineService.create(record.memberId, prevRound.id, 'absent'); + } catch (fineError) { + Sentry.captureException(fineError); + logger.error({ memberId: record.memberId, error: fineError }, '✅ [결석 처리] 벌금 부과 실패'); + } + } + } catch (error) { + Sentry.captureException(error); + logger.error({ error }, '✅ [결석 처리] 에러'); + } }); await boss.work('fine-reminder', { batchSize: 1 }, async () => { diff --git a/packages/bot/src/schedulers/index.ts b/packages/bot/src/schedulers/index.ts index a515445..52d189f 100644 --- a/packages/bot/src/schedulers/index.ts +++ b/packages/bot/src/schedulers/index.ts @@ -2,7 +2,6 @@ // 모든 스케줄러를 내보냅니다. export * from './rss-poller'; -export * from './attendance-checker'; export * from './fine-reminder'; export * from './round-reporter'; export * from './curation-crawler'; diff --git a/packages/bot/src/schedulers/round-reporter.ts b/packages/bot/src/schedulers/round-reporter.ts index 02e59d3..2e6eed5 100644 --- a/packages/bot/src/schedulers/round-reporter.ts +++ b/packages/bot/src/schedulers/round-reporter.ts @@ -6,8 +6,8 @@ import { Client } from 'discord.js'; import { and, count, eq } from 'drizzle-orm'; import logger from '../lib/logger'; -import { attendance, getDb, members, posts, type Round, rounds, } from '@blog-study/shared/db'; -import { getCurrentRound, getRoundByNumber, isGracePeriodEnded, setCurrentRound, } from '../services/round.service'; +import { attendance, getDb, members, posts, type Round } from '@blog-study/shared/db'; +import { getCurrentRound, getRoundByNumber, isGracePeriodEnded } from '../services/round.service'; import { type AttendanceSummary, calculateRoundReportData, @@ -112,12 +112,27 @@ export class RoundReporter { try { const currentRound = await getCurrentRound(); + // 현재 회차의 이전 회차(종료된 회차)를 가져와서 리포트 발송 + const prevRound = await getRoundByNumber(currentRound.roundNumber - 1); + + if (!prevRound) { + logger.info('📊 [회차 리포트] 이전 회차 없음, 건너뜀'); + return { + timestamp: startTime, + roundNumber: 0, + reportSent: false, + newRoundStarted: false, + newRoundNumber: null, + errors: ['이전 회차 없음'], + }; + } + // grace period 체크 (수동 트리거 시 건너뜀) - if (!force && !isGracePeriodEnded(currentRound)) { - logger.info(`📊 [회차 리포트] ${currentRound.roundNumber}회차 지각 기간 미종료, 건너뜀`); + if (!force && !isGracePeriodEnded(prevRound)) { + logger.info(`📊 [회차 리포트] ${prevRound.roundNumber}회차 지각 기간 미종료, 건너뜀`); return { timestamp: startTime, - roundNumber: currentRound.roundNumber, + roundNumber: prevRound.roundNumber, reportSent: false, newRoundStarted: false, newRoundNumber: null, @@ -125,9 +140,9 @@ export class RoundReporter { }; } - logger.info(`📊 [회차 리포트] ${currentRound.roundNumber}회차 리포트 생성 중...`); + logger.info(`📊 [회차 리포트] ${prevRound.roundNumber}회차 리포트 생성 중...`); - const reportData = await buildRoundReportDataForRound(currentRound); + const reportData = await buildRoundReportDataForRound(prevRound); const notificationService = getNotificationService(); const sent = await notificationService.sendRoundReport(reportData); @@ -135,28 +150,14 @@ export class RoundReporter { errors.push('리포트 발송 실패'); } - logger.info(`📊 [회차 리포트] ${currentRound.roundNumber}회차 리포트 ${sent ? '발송 완료 ✅' : '발송 실패 ❌'}`); - - // 다음 회차로 전환 - const nextRound = await getRoundByNumber(currentRound.roundNumber + 1); - if (nextRound) { - await setCurrentRound(nextRound.roundNumber); - logger.info(`📊 [회차 리포트] 현재 회차 → ${nextRound.roundNumber}회차로 전환`); - } else { - const db = getDb(); - await db - .update(rounds) - .set({ isCurrent: false }) - .where(eq(rounds.id, currentRound.id)); - logger.info(`📊 [회차 리포트] 다음 회차 없음, ${currentRound.roundNumber}회차 종료`); - } + logger.info(`📊 [회차 리포트] ${prevRound.roundNumber}회차 리포트 ${sent ? '발송 완료 ✅' : '발송 실패 ❌'}`); return { timestamp: startTime, - roundNumber: currentRound.roundNumber, + roundNumber: prevRound.roundNumber, reportSent: sent, - newRoundStarted: nextRound !== null, - newRoundNumber: nextRound?.roundNumber ?? null, + newRoundStarted: false, + newRoundNumber: null, errors, }; } catch (error) { @@ -203,6 +204,7 @@ export class RoundReporter { const todayStr = formatKSTDate(new Date()); const isTodayRoundStart = todayStr === currentRound.startDate; + // attendance-init(00:02)에서 이미 회차 전환 완료 — 현재 회차 기준으로 알림만 발송 if (force || isTodayRoundStart) { logger.info(`🚀 [회차 시작] ${currentRound.roundNumber}회차 시작 알림 발송 중...`); @@ -225,30 +227,6 @@ export class RoundReporter { }; } - // 다음 회차 시작일인지 확인 - const nextRound = await getRoundByNumber(currentRound.roundNumber + 1); - - if (nextRound && (force || todayStr === nextRound.startDate)) { - await setCurrentRound(nextRound.roundNumber); - logger.info(`🚀 [회차 시작] ${nextRound.roundNumber}회차 시작, 회차 전환 완료`); - - const notificationService = getNotificationService(); - const sent = await notificationService.sendRoundStartAnnouncement(nextRound); - - if (!sent) { - errors.push('회차 시작 알림 발송 실패'); - } - - return { - timestamp: startTime, - roundNumber: nextRound.roundNumber, - reportSent: false, - newRoundStarted: sent, - newRoundNumber: nextRound.roundNumber, - errors, - }; - } - logger.info(`🚀 [회차 시작] 오늘(${todayStr})은 회차 시작일이 아님, 건너뜀`); return { diff --git a/packages/bot/src/services/notification.service.ts b/packages/bot/src/services/notification.service.ts index 85b0f01..069dbd6 100644 --- a/packages/bot/src/services/notification.service.ts +++ b/packages/bot/src/services/notification.service.ts @@ -185,7 +185,7 @@ export interface RoundReportData { * Requirements: 10.3 - Highlight MVP */ export function buildRoundReportEmbed(data: RoundReportData): EmbedBuilder { - const { round, submitted, late, absent, mvps, submissionRate, lateRate, absentRate } = data; + const { round, submitted, mvps, submissionRate, lateRate, absentRate } = data; const embed = new EmbedBuilder() .setColor(0x57F287) // Green @@ -226,30 +226,6 @@ export function buildRoundReportEmbed(data: RoundReportData): EmbedBuilder { }); } - // Late list - if (late.length > 0) { - const lateList = late - .map((l) => `• <@${l.discordId}> (${l.name})`) - .join('\n'); - embed.addFields({ - name: `⏰ 지각 (${late.length}명)`, - value: lateList.length > 1024 ? lateList.substring(0, 1021) + '...' : lateList, - inline: true, - }); - } - - // Absent list - if (absent.length > 0) { - const absentList = absent - .map((a) => `• <@${a.discordId}> (${a.name})`) - .join('\n'); - embed.addFields({ - name: `❌ 결석 (${absent.length}명)`, - value: absentList.length > 1024 ? absentList.substring(0, 1021) + '...' : absentList, - inline: true, - }); - } - return embed; } diff --git a/packages/web/src/app/(user)/dashboard/page.tsx b/packages/web/src/app/(user)/dashboard/page.tsx index 009022b..3493e57 100644 --- a/packages/web/src/app/(user)/dashboard/page.tsx +++ b/packages/web/src/app/(user)/dashboard/page.tsx @@ -76,23 +76,31 @@ function getGreeting(): { emoji: string; text: string } { return { emoji: '🌆', text: '오늘 하루도 수고했어요.' }; } -function getMotivation(round: RoundInfo | null): { +function getMotivation( + round: RoundInfo | null, + myStatus: string | null, +): { emoji: string; message: string; tone: 'chill' | 'warn' | 'urgent' | 'celebrate'; } { if (!round) return { emoji: '📝', message: '새 회차를 기다리는 중이에요', tone: 'chill' }; + // 활성 + 미제출(PENDING/null) 유저만 마감 압박 메시지 표시 + const isActiveAndPending = + myStatus === 'active' && + (!round.myAttendanceStatus || round.myAttendanceStatus === 'PENDING'); + if (round.submissionRate >= 100) { return { emoji: '🎉', message: '이번 회차 전원 제출 완료! 다들 멋져요', tone: 'celebrate' }; } - if (round.isGracePeriod) { + if (isActiveAndPending && round.isGracePeriod) { return { emoji: '😱', message: '지각 기간이에요! 서둘러 제출해주세요', tone: 'urgent' }; } - if (round.daysRemaining <= 1) { + if (isActiveAndPending && round.daysRemaining <= 1) { return { emoji: '⏰', message: '마감이 코앞이에요! 오늘 안에 제출하세요', tone: 'urgent' }; } - if (round.daysRemaining <= 3) { + if (isActiveAndPending && round.daysRemaining <= 3) { return { emoji: '🔥', message: '마감이 다가오고 있어요, 슬슬 준비해볼까요?', tone: 'warn' }; } if (round.submissionRate >= 80) { @@ -215,7 +223,7 @@ export default function DashboardPage() { } const greeting = getGreeting(); - const motivation = getMotivation(data?.currentRound ?? null); + const motivation = getMotivation(data?.currentRound ?? null, data?.myStatus ?? null); const round = data?.currentRound; const ddayColor = round From 799eb923523ac5eaefcf143d15ee8793865c3713 Mon Sep 17 00:00:00 2001 From: bbbang105 <2018111366@dgu.ac.kr> Date: Mon, 6 Apr 2026 11:13:04 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=E2=80=94=20=EC=88=98=EB=8F=99=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=20=EC=9D=B4=EC=A0=84=20=ED=9A=8C=EC=B0=A8=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20+=20grace=20period=20=EA=B0=80=EB=93=9C=20+=20dead?= =?UTF-8?q?=20fields=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- packages/bot/src/api-server.ts | 20 ++++++++++++++----- .../bot/src/services/notification.service.ts | 8 ++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/bot/src/api-server.ts b/packages/bot/src/api-server.ts index f772ab6..70fa06c 100644 --- a/packages/bot/src/api-server.ts +++ b/packages/bot/src/api-server.ts @@ -18,7 +18,7 @@ import { } from './schedulers'; import { getAttendanceService } from './services/attendance.service'; import { getFineService } from './services'; -import { getCurrentRound } from './services/round.service'; +import { getCurrentRound, getRoundByNumber, isGracePeriodEnded } from './services/round.service'; import { AttendanceStatus } from '@blog-study/shared/db'; const BOT_API_SECRET = process.env.BOT_API_SECRET; @@ -83,23 +83,33 @@ export function createBotApiServer(): Express { app.post('/api/trigger/attendance-check', authMiddleware, triggerLimiter, async (_req, res) => { try { const currentRound = await getCurrentRound(); + const prevRound = await getRoundByNumber(currentRound.roundNumber - 1); + + if (!prevRound) { + return res.status(400).json({ error: '이전 회차가 없습니다' }); + } + + if (!isGracePeriodEnded(prevRound)) { + return res.status(400).json({ error: '이전 회차 유예 기간이 아직 종료되지 않았습니다' }); + } + const attendanceService = getAttendanceService(); const fineService = getFineService(); - // PENDING → ABSENT 처리 - const processedRecords = await attendanceService.processGracePeriodEnd(currentRound.id); + // 이전 회차 PENDING → ABSENT 처리 + const processedRecords = await attendanceService.processGracePeriodEnd(prevRound.id); const absentRecords = processedRecords.filter(r => r.status === AttendanceStatus.ABSENT); // 결석 벌금 부과 for (const record of absentRecords) { try { - await fineService.create(record.memberId, currentRound.id, 'absent'); + await fineService.create(record.memberId, prevRound.id, 'absent'); } catch (fineError) { logger.error({ memberId: record.memberId, error: fineError }, '🌐 [API] 결석 벌금 부과 실패'); } } - res.json({ success: true, result: { processedCount: absentRecords.length } }); + res.json({ success: true, result: { roundNumber: prevRound.roundNumber, processedCount: absentRecords.length } }); } catch (error) { Sentry.captureException(error); logger.error({ error }, '🌐 [API] 출석 체크 에러'); diff --git a/packages/bot/src/services/notification.service.ts b/packages/bot/src/services/notification.service.ts index 069dbd6..7242169 100644 --- a/packages/bot/src/services/notification.service.ts +++ b/packages/bot/src/services/notification.service.ts @@ -13,8 +13,6 @@ import { type MessageCreateOptions, TextChannel, } from 'discord.js'; - -const KUSTING_WEB_URL = 'https://kusting-web.vercel.app'; import type { AttendanceStatusType, Member, Post, Round } from '@blog-study/shared/db'; import { AttendanceStatus, getDb, members, MemberStatus } from '@blog-study/shared/db'; import { eq } from 'drizzle-orm'; @@ -22,6 +20,8 @@ import { ConfigKeys, getConfigValue } from './round.service'; import logger from '../lib/logger'; import { logNotification } from '../lib/notification-logger'; +const KUSTING_WEB_URL = 'https://kusting-web.vercel.app'; + /** * Error codes for notification operations */ @@ -170,8 +170,6 @@ export interface AttendanceSummary { export interface RoundReportData { round: Round; submitted: AttendanceSummary[]; - late: AttendanceSummary[]; - absent: AttendanceSummary[]; mvps: AttendanceSummary[]; totalMembers: number; submissionRate: number; @@ -321,8 +319,6 @@ export function calculateRoundReportData( return { round, submitted, - late, - absent, mvps, totalMembers, submissionRate, From 236f259fc5d80caab754b177da69595d44facda7 Mon Sep 17 00:00:00 2001 From: bbbang105 <2018111366@dgu.ac.kr> Date: Mon, 6 Apr 2026 11:18:43 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20property=20test=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=EB=90=9C=20late/absent=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=B0=B8=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- .../bot/src/services/notification.service.property.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/bot/src/services/notification.service.property.test.ts b/packages/bot/src/services/notification.service.property.test.ts index 65a3a6e..1bd1cc6 100644 --- a/packages/bot/src/services/notification.service.property.test.ts +++ b/packages/bot/src/services/notification.service.property.test.ts @@ -344,8 +344,8 @@ describe('NotificationService Property Tests', () => { const expectedAbsent = summaries.filter(s => s.status === AttendanceStatus.ABSENT).length; expect(reportData.submitted.length).toBe(expectedSubmitted); - expect(reportData.late.length).toBe(expectedLate); - expect(reportData.absent.length).toBe(expectedAbsent); + expect(reportData.lateRate).toBeCloseTo(expectedLate / summaries.length, 5); + expect(reportData.absentRate).toBeCloseTo(expectedAbsent / summaries.length, 5); } ), { numRuns: 100 } @@ -476,8 +476,6 @@ describe('NotificationService Property Tests', () => { const reportData = calculateRoundReportData(round, []); expect(reportData.submitted).toHaveLength(0); - expect(reportData.late).toHaveLength(0); - expect(reportData.absent).toHaveLength(0); expect(reportData.mvps).toHaveLength(0); expect(reportData.totalMembers).toBe(0); expect(reportData.submissionRate).toBe(0);