Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

## 핵심 파일 위치
Expand All @@ -104,6 +106,7 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이)
| `packages/bot/src/bot.ts` | Discord 클라이언트 초기화 (이벤트 핸들러만) |
| `packages/bot/src/job-queue.ts` | pg-boss 싱글톤 (시작/종료/조회) |
| `packages/bot/src/scheduler-registry.ts` | 잡 등록 + RSS→Post→Notification→Push 파이프라인 |
| `packages/bot/src/schedulers/popular-posts.ts` | 인기 포스트 TOP 5 Discord 알림 (화 08:01 KST + 수동) |
| `packages/bot/src/services/score.service.ts` | 활동 점수 계산/부여 (봇: blog_post만) |
| `packages/web/src/lib/score.ts` | 웹 활동 점수 부여 (board_post, post_comment, board_comment, post_view) |
| `packages/web/src/lib/score-config.ts` | 활동 점수 타입별 메타데이터 (Single Source of Truth: 라벨, 이모지, 배점, 뱃지 컬러) |
Expand Down
31 changes: 31 additions & 0 deletions packages/bot/src/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getDeadlineReminder,
getFineReminder,
getPollReminder,
getPopularPosts,
getRoundReporter,
getRssPoller,
getWeeklyRanking,
Expand Down Expand Up @@ -259,6 +260,36 @@ export function createBotApiServer(): Express {
}
});

app.post('/api/trigger/popular-posts', authMiddleware, triggerLimiter, async (req, res) => {
try {
const popularPosts = getPopularPosts();

if (popularPosts.isSending()) {
return res.status(409).json({ error: '인기 포스트 알림이 이미 실행 중입니다' });
}

const { roundNumber } = req.body || {};

const result = await popularPosts.sendPopularPosts(
true,
typeof roundNumber === 'number' ? roundNumber : undefined
);

const serializedResult = {
...result,
timestamp: result.timestamp instanceof Date
? result.timestamp.toISOString()
: result.timestamp,
};

res.json({ success: true, result: serializedResult });
} catch (error) {
Sentry.captureException(error);
logger.error({ error }, '🌐 [API] 인기 포스트 에러');
res.status(500).json({ error: '내부 오류가 발생했습니다' });
}
});

app.post('/api/trigger/deadline-reminder', authMiddleware, triggerLimiter, async (req, res) => {
try {
const deadlineReminder = getDeadlineReminder();
Expand Down
10 changes: 10 additions & 0 deletions packages/bot/src/scheduler-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getRoundReporter } from './schedulers/round-reporter';
import { getCurationCrawler } from './schedulers/curation-crawler';
import { getWeeklyRanking } from './schedulers/weekly-ranking';
import { getDeadlineReminder } from './schedulers/deadline-reminder';
import { getPopularPosts } from './schedulers/popular-posts';
import type { CrawledContent } from './services/curation.service';
import { getPostService } from './services/post.service';
import { getNotificationService } from './services/notification.service';
Expand Down Expand Up @@ -41,6 +42,7 @@ const JOB_DEFINITIONS = [
{ name: 'curation-share', cron: '5 10 * * *' }, // 4기 미사용
{ name: 'weekly-ranking', cron: '0 1 * * 0' }, // KST 일 10:00 (UTC 일 01:00)
{ name: 'deadline-reminder', cron: '0 23 * * *' }, // KST 매일 08:00 (UTC 23:00)
{ name: 'popular-posts', cron: '5 23 * * 1' }, // KST 화 08:05 (UTC 월 23:05)
] as const;

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

fineReminder.setClient(client);
roundReporter.setClient(client);
curationCrawler.setClient(client);
weeklyRanking.setClient(client);
deadlineReminder.setClient(client);
popularPosts.setClient(client);

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

await boss.createQueue('popular-posts');

await boss.work('popular-posts', { batchSize: 1 }, async () => {
await popularPosts.sendPopularPosts();
});

// Wait for queues to be created in the database
await new Promise(resolve => setTimeout(resolve, 500));

Expand Down
1 change: 1 addition & 0 deletions packages/bot/src/schedulers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './curation-crawler';
export * from './weekly-ranking';
export * from './poll-reminder';
export * from './deadline-reminder';
export * from './popular-posts';
Loading
Loading