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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ SENTRY_DSN=https://your-dsn@o123.ingest.us.sentry.io/456
BOT_API_SECRET=your_bot_api_secret_here
BOT_API_URL=http://localhost:3001

# Internal API (bot → web push notification)
INTERNAL_API_KEY=your_internal_api_key_here
WEB_URL=https://your-web-app.vercel.app

# Application
APP_URL=http://localhost:3000
NODE_ENV=development
Expand Down
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이)
- **Discord 알림**: 웹에서 직접 Discord REST API 호출 시 `discord-notify.ts` 유틸 사용, 사용자 입력은 `escapeDiscordMarkdown()` 적용, `allowed_mentions: { parse: [] }` 필수
- **댓글 길이**: 최대 5000자 제한 (API에서 검증)
- **이미지 업로드**: Cloudflare R2 (`board-images/{userId}/{uuid}.{ext}`), 5MB 제한, rate limit 20회/분/유저
- **포스트 수동등록**: 2단계 UX (URL→미리보기→편집→등록), OG HTML 엔티티 자동 디코딩, Discord 알림 토글
- **포스트 수동등록**: 2단계 UX (URL→미리보기→편집→등록), OG HTML 엔티티 자동 디코딩, Discord 알림 토글, 푸시 알림은 Discord 토글과 무관하게 항상 발송
- **새 글 푸시 알림**: 수동 등록 + RSS 수집 모두 지원. 대상: active/OB/dormant (작성자 본인 제외), 알림 타입 `new_post`. 봇→웹 내부 API(`/api/internal/new-post-push`, Bearer 인증) 경유
- **포스트 수정**: 본인 또는 관리자만 제목/설명 수정 가능 (`PATCH /api/posts/[id]`)
- **공지 알림**: 게시판 공지 작성 시 FCM 푸시 + Discord 공지채널(`notice_channel_id`) `@everyone` + 웹 딥링크 버튼
- **벌금 DM**: 계좌 정보 포함 (3333333114501 카카오뱅크), 납부완료 시 관리자 채널 알림
Expand Down Expand Up @@ -98,15 +99,15 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이)
| `packages/bot/src/lib/sentry.ts` | 봇 Sentry SDK 초기화 (PII 스크러빙, DB URL/토큰 마스킹) |
| `packages/bot/src/bot.ts` | Discord 클라이언트 초기화 (이벤트 핸들러만) |
| `packages/bot/src/job-queue.ts` | pg-boss 싱글톤 (시작/종료/조회) |
| `packages/bot/src/scheduler-registry.ts` | 잡 등록 + RSS→Post→Notification 파이프라인 |
| `packages/bot/src/scheduler-registry.ts` | 잡 등록 + RSS→Post→Notification→Push 파이프라인 |
| `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: 라벨, 이모지, 배점, 뱃지 컬러) |
| `packages/web/src/app/(user)/profile/activity/page.tsx` | 활동 내역 페이지 (타입별 필터, 무한 로드) |
| `packages/web/src/lib/board-auth.ts` | 게시판 인증 헬퍼 (`getBoardAuth`) |
| `packages/web/src/lib/board-config.ts` | 게시판 카테고리/뱃지 설정 |
| `packages/web/src/lib/api-error.ts` | API 표준 응답/에러 헬퍼 (`successResponse`, `Errors`, `withCache`) |
| `packages/web/src/lib/sanitize.ts` | 입력 새니타이즈 (`sanitizeDescription`, `sanitizeTiptapContent`, `getTodayKST`) |
| `packages/web/src/lib/sanitize.ts` | 입력 새니타이즈 (`sanitizeDescription`, `sanitizeTiptapContent`, `decodeHtmlEntities`, `getTodayKST`) |
| `packages/web/src/app/not-found.tsx` | 커스텀 404 페이지 |
| `packages/web/src/app/(user)/error.tsx` | 사용자 에러 바운더리 |
| `packages/web/src/app/(admin)/error.tsx` | 관리자 에러 바운더리 |
Expand All @@ -130,6 +131,7 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이)
| `packages/web/src/components/settings/push-notification-settings.tsx` | 알림 설정 UI (타입별 토글 + 테스트 전송) |
| `packages/web/src/app/api/push/test/route.ts` | 테스트 푸시 알림 API (레이트 리밋 5/min) |
| `packages/web/src/app/api/notification-preferences/route.ts` | 알림 타입별 설정 CRUD API |
| `packages/web/src/app/api/internal/new-post-push/route.ts` | 새 글 푸시 알림 내부 API (봇→웹, Bearer 인증, rate limit 20/min) |
| `packages/web/src/app/api/firebase-sw/route.ts` | FCM 서비스 워커 동적 서빙 (rewrite: `/firebase-messaging-sw.js` → `/api/firebase-sw`) |
| `packages/bot/src/scripts/rss-collect.ts` | 수동 RSS 수집 스크립트 (봇 없이 독립 실행) |
| `packages/bot/src/scripts/setup-channels.ts` | 디스코드 채널 일괄 생성 스크립트 |
Expand Down Expand Up @@ -245,6 +247,8 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이)
- `SENTRY_AUTH_TOKEN` (소스맵 업로드, Vercel/CI에서만 설정)
- `FIREBASE_PROJECT_ID`, `FIREBASE_PRIVATE_KEY`, `FIREBASE_CLIENT_EMAIL` 등 (Firebase Admin, 서버용)
- `NEXT_PUBLIC_FIREBASE_*` (Firebase 클라이언트, `API_KEY`/`AUTH_DOMAIN`/`PROJECT_ID`/`MESSAGING_SENDER_ID`/`APP_ID`/`VAPID_KEY`)
- `INTERNAL_API_KEY` (봇→웹 내부 API 인증, 웹+봇 공유)
- `WEB_URL` (봇에서 웹 API 호출 시 base URL, 봇 전용)

**env 파일 위치** (2곳):
- `.env.local` — 루트 (shared/bot용)
Expand Down
9 changes: 6 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Blog Study Admin - 시스템 아키텍처

> 최종 업데이트: 2026-03-18 (v11)
> 최종 업데이트: 2026-03-23 (v12)

블로그 글쓰기 스터디 운영 자동화 플랫폼. 웹 대시보드에서 모든 관리/유저 기능을 제공하고, Discord 봇은 스케줄러(RSS 수집/출석/벌금/큐레이션)와 이벤트 핸들러만 담당한다.

Expand Down Expand Up @@ -53,7 +53,7 @@ graph TB
BANNER["Notice Banner<br/>글로벌 공지 배너"]
PWA["PWA<br/>manifest.json<br/>홈 화면 추가"]
FCM["FCM Push<br/>firebase-admin · firebase/messaging<br/>서비스 워커"]
API["API Routes<br/>/api/auth · /api/posts<br/>/api/admin · /api/board<br/>/api/push · /api/notification-preferences"]
API["API Routes<br/>/api/auth · /api/posts<br/>/api/admin · /api/board<br/>/api/push · /api/notification-preferences<br/>/api/internal (봇→웹 내부 API)"]
SUPA_CLIENT["Supabase SSR Client<br/>@supabase/ssr"]
SENTRY_WEB["Sentry SDK<br/>에러 모니터링 + PII 스크러빙"]
end
Expand All @@ -70,6 +70,7 @@ graph TB
Bot -->|service_role key| TABLES
API -->|POST /api/trigger/*| BOT_API
BOT_API --> SCH
BOT_API -->|POST /api/internal/new-post-push| API
API -->|Discord REST API| CH_ADMIN

Web -->|HTTPS| DB
Expand Down Expand Up @@ -218,6 +219,7 @@ flowchart TD
D --> E["출석 상태 업데이트"]
E --> F["활동 점수 부여 (봇: blog_post)"]
F --> G["Discord 채널 알림"]
G --> H["FCM 푸시 알림<br/>(웹 내부 API 호출, fire-and-forget)"]
```

### 큐레이션 추천 흐름
Expand Down Expand Up @@ -451,7 +453,7 @@ erDiagram
- **Pull-to-Refresh**: 커스텀 터치 제스처 기반 새로고침 (`PullToRefresh` + `usePullToRefresh`), Safari PWA 최적화, 다이얼로그 열림 시 `data-scroll-locked` 가드로 비활성화
- **PWA**: `manifest.json` + 커스텀 로고 아이콘 (SVG/192/512, maskable) → 홈 화면 추가 지원
- **OG 이미지**: `opengraph-image.tsx` Edge Runtime 동적 생성 (1200×630, `next/og` ImageResponse). 다크 테마 + K 로고 + 히어로 카피 + Mock UI 카드 (랭킹/포스트). `layout.tsx`에 `openGraph`/`twitter` 메타데이터 + `og:url`
- **FCM 푸시**: Firebase Cloud Messaging 서비스 워커 (API route `/api/firebase-sw` → rewrite `/firebase-messaging-sw.js`) → 백그라운드 알림. 타입별(댓글/답글/공지) 개별 설정, 테스트 알림 전송 지원. 푸시/점수 등 백그라운드 작업은 `after()` from `next/server` 사용
- **FCM 푸시**: Firebase Cloud Messaging 서비스 워커 (API route `/api/firebase-sw` → rewrite `/firebase-messaging-sw.js`) → 백그라운드 알림. 타입별(댓글/답글/공지/새글) 개별 설정, 테스트 알림 전송 지원. 새 글 알림은 수동 등록(`after()`) + RSS 수집(봇→웹 내부 API) 모두 지원. 푸시/점수 등 백그라운드 작업은 `after()` from `next/server` 사용

## 스케줄러 (pg-boss)

Expand Down Expand Up @@ -479,6 +481,7 @@ erDiagram
| **SQL Injection** | Drizzle ORM 파라미터화 쿼리 (raw SQL 사용 안 함) | 전체 API Routes |
| **CSRF** | Supabase Auth 쿠키 `SameSite=Lax` | Supabase 기본 설정 |
| **입력 검증** | description 새니타이즈 (제어 문자/제로 너비 유니코드 제거, 300자 제한) | `lib/sanitize.ts` |
| **내부 API 인증** | Bearer 토큰 (`INTERNAL_API_KEY`, timing-safe 비교) + rate limit 20/min + UUID/길이 검증 | `api/internal/new-post-push/` |

### 에러 처리

Expand Down
Loading
Loading