Skip to content

Commit a910482

Browse files
bbbang105claude
andauthored
feat: 인기 포스트 TOP 5 알림 + 회차별 필터 + 메달 5위 확장 (#77) (#77)
- 인기 포스트 Discord 알림 스케줄러 (화 08:05 KST 자동 + 수동 트리거) - 포스트 회차별 필터 드롭다운 (동적 회차 목록) - 인기순 메달 테두리 3위→5위 확장 (4위 스카이블루, 5위 라벤더) - 관리자 설정에 인기 포스트 채널 ID 추가 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 967966e commit a910482

13 files changed

Lines changed: 530 additions & 55 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이)
8181
- **비밀댓글 isSecret 토글**: PATCH 시 본인만 변경 가능 (관리자도 타인 비밀 상태 변경 불가)
8282
- **포스트 삭제**: 본인 또는 관리자만 가능, 트랜잭션으로 댓글/조회기록/활동점수(blog_post) 일괄 삭제
8383
- **이모지 리액션**: 게시판 글 + 포스트에 고정 6종 이모지 (👍👀🔥💡😂✅) 토글, `ReactionBar` 공용 컴포넌트 (`apiPath` prop으로 board/posts 구분), 호버(PC)/클릭(모바일) 시 닉네임 팝오버, 복수 선택 가능, 활동 점수/알림 없음
84-
- **인기글 점수**: `댓글×3 + 조회수×2 + 리액션×1`
84+
- **인기글 점수**: `댓글×3 + 조회수×2 + 리액션×1`, 인기순 상위 5개 메달 테두리 (금/은/동/스카이블루/라벤더)
85+
- **인기 포스트 알림**: 화 08:05 KST 자동 + 수동 트리거, 이전 회차 TOP 5 Discord Embed (이모지별 카운트, 썸네일, 링크 버튼), `popular_posts_channel_id` 설정 필요
86+
- **포스트 회차 필터**: 전체/회차별 셀렉트 드롭다운, `/api/rounds`에서 동적 조회, 인기순도 선택 회차 기준
8587
- **스터디원 목록**: active + dormant + ob 모두 표시, 상태 칩으로 구분 (OB: 황금 파스텔, 휴면: secondary)
8688

8789
## 핵심 파일 위치
@@ -104,6 +106,7 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이)
104106
| `packages/bot/src/bot.ts` | Discord 클라이언트 초기화 (이벤트 핸들러만) |
105107
| `packages/bot/src/job-queue.ts` | pg-boss 싱글톤 (시작/종료/조회) |
106108
| `packages/bot/src/scheduler-registry.ts` | 잡 등록 + RSS→Post→Notification→Push 파이프라인 |
109+
| `packages/bot/src/schedulers/popular-posts.ts` | 인기 포스트 TOP 5 Discord 알림 (화 08:01 KST + 수동) |
107110
| `packages/bot/src/services/score.service.ts` | 활동 점수 계산/부여 (봇: blog_post만) |
108111
| `packages/web/src/lib/score.ts` | 웹 활동 점수 부여 (board_post, post_comment, board_comment, post_view) |
109112
| `packages/web/src/lib/score-config.ts` | 활동 점수 타입별 메타데이터 (Single Source of Truth: 라벨, 이모지, 배점, 뱃지 컬러) |

packages/bot/src/api-server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getDeadlineReminder,
1313
getFineReminder,
1414
getPollReminder,
15+
getPopularPosts,
1516
getRoundReporter,
1617
getRssPoller,
1718
getWeeklyRanking,
@@ -259,6 +260,36 @@ export function createBotApiServer(): Express {
259260
}
260261
});
261262

263+
app.post('/api/trigger/popular-posts', authMiddleware, triggerLimiter, async (req, res) => {
264+
try {
265+
const popularPosts = getPopularPosts();
266+
267+
if (popularPosts.isSending()) {
268+
return res.status(409).json({ error: '인기 포스트 알림이 이미 실행 중입니다' });
269+
}
270+
271+
const { roundNumber } = req.body || {};
272+
273+
const result = await popularPosts.sendPopularPosts(
274+
true,
275+
typeof roundNumber === 'number' ? roundNumber : undefined
276+
);
277+
278+
const serializedResult = {
279+
...result,
280+
timestamp: result.timestamp instanceof Date
281+
? result.timestamp.toISOString()
282+
: result.timestamp,
283+
};
284+
285+
res.json({ success: true, result: serializedResult });
286+
} catch (error) {
287+
Sentry.captureException(error);
288+
logger.error({ error }, '🌐 [API] 인기 포스트 에러');
289+
res.status(500).json({ error: '내부 오류가 발생했습니다' });
290+
}
291+
});
292+
262293
app.post('/api/trigger/deadline-reminder', authMiddleware, triggerLimiter, async (req, res) => {
263294
try {
264295
const deadlineReminder = getDeadlineReminder();

packages/bot/src/scheduler-registry.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getRoundReporter } from './schedulers/round-reporter';
1313
import { getCurationCrawler } from './schedulers/curation-crawler';
1414
import { getWeeklyRanking } from './schedulers/weekly-ranking';
1515
import { getDeadlineReminder } from './schedulers/deadline-reminder';
16+
import { getPopularPosts } from './schedulers/popular-posts';
1617
import type { CrawledContent } from './services/curation.service';
1718
import { getPostService } from './services/post.service';
1819
import { getNotificationService } from './services/notification.service';
@@ -41,6 +42,7 @@ const JOB_DEFINITIONS = [
4142
{ name: 'curation-share', cron: '5 10 * * *' }, // 4기 미사용
4243
{ name: 'weekly-ranking', cron: '0 1 * * 0' }, // KST 일 10:00 (UTC 일 01:00)
4344
{ name: 'deadline-reminder', cron: '0 23 * * *' }, // KST 매일 08:00 (UTC 23:00)
45+
{ name: 'popular-posts', cron: '5 23 * * 1' }, // KST 화 08:05 (UTC 월 23:05)
4446
] as const;
4547

4648
/**
@@ -56,12 +58,14 @@ export async function registerAllJobs(boss: PgBoss, client: Client): Promise<voi
5658
const curationCrawler = getCurationCrawler();
5759
const weeklyRanking = getWeeklyRanking();
5860
const deadlineReminder = getDeadlineReminder();
61+
const popularPosts = getPopularPosts();
5962

6063
fineReminder.setClient(client);
6164
roundReporter.setClient(client);
6265
curationCrawler.setClient(client);
6366
weeklyRanking.setClient(client);
6467
deadlineReminder.setClient(client);
68+
popularPosts.setClient(client);
6569

6670
// Set up RSS poller callback: new post → save to DB + send notification + grant score + update attendance
6771
const postService = getPostService();
@@ -338,6 +342,12 @@ export async function registerAllJobs(boss: PgBoss, client: Client): Promise<voi
338342
await deadlineReminder.sendReminders();
339343
});
340344

345+
await boss.createQueue('popular-posts');
346+
347+
await boss.work('popular-posts', { batchSize: 1 }, async () => {
348+
await popularPosts.sendPopularPosts();
349+
});
350+
341351
// Wait for queues to be created in the database
342352
await new Promise(resolve => setTimeout(resolve, 500));
343353

packages/bot/src/schedulers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './curation-crawler';
88
export * from './weekly-ranking';
99
export * from './poll-reminder';
1010
export * from './deadline-reminder';
11+
export * from './popular-posts';

0 commit comments

Comments
 (0)