diff --git a/.env.example b/.env.example
index 4e78782..206e80d 100644
--- a/.env.example
+++ b/.env.example
@@ -16,9 +16,6 @@ DATABASE_URL_DIRECT=postgresql://user:password@host:5432/database
APP_URL=http://localhost:3000
NODE_ENV=development
-# OpenAI (AI 기능용)
-OPENAI_API_KEY=your_openai_api_key
-
# Study Configuration (optional, can be set via admin commands)
STUDY_START_DATE=2024-01-01
TOTAL_ROUNDS=10
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index ef29017..82b4174 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @bbbang105
+* @bbbang105 @choihooo
diff --git a/.github/auto-assign-config.yaml b/.github/auto-assign-config.yaml
index 649c44e..340c2d2 100644
--- a/.github/auto-assign-config.yaml
+++ b/.github/auto-assign-config.yaml
@@ -2,3 +2,4 @@ addReviewers: true
addAssignees: author
reviewers:
- bbbang105
+ - choihooo
diff --git a/.gitignore b/.gitignore
index 75e12ff..a1877e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,7 +34,9 @@ coverage
*.tsbuildinfo
### CLAUDE ###
-/docs
+#/docs
# Drizzle migration SQL (로컬 전용, DB에서 직접 실행)
packages/shared/drizzle/*.sql
+.vercel
+scripts
diff --git a/CLAUDE.md b/CLAUDE.md
index b510113..5cad7de 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,13 +1,13 @@
# Blog Study Discord Bot
-블로그 글쓰기 스터디 자동화 플랫폼. Discord 봇 + 웹 대시보드 + AI 추천.
+블로그 글쓰기 스터디 자동화 플랫폼. Discord 봇 + 웹 대시보드.
## 프로젝트 구조
```
packages/
├── bot/ # Discord 봇 (discord.js v14) → AWS EC2 배포
-├── web/ # Next.js 14 대시보드 → Vercel 배포
+├── web/ # Next.js 16 대시보드 → Vercel 배포
└── shared/ # 공유 코드 (DB 스키마, 타입, 유틸)
```
@@ -22,7 +22,6 @@ packages/
| Web | Next.js 16 App Router, React 19, shadcn/ui, Tailwind CSS v4, Tiptap (리치 에디터) |
| DB | Supabase PostgreSQL + Drizzle ORM (Transaction Pooler, `prepare: false`) |
| Auth | Supabase Auth (Discord OAuth) + `@supabase/ssr` |
-| AI | OpenAI GPT-4o-mini + text-embedding-3-small (예정) |
| 배포 | AWS EC2 (bot), Vercel (web), Supabase (DB + Auth) |
## 개발 명령어
@@ -114,7 +113,7 @@ pnpm --filter @blog-study/bot init-rounds # 회차 초기화
- **폰트**: Pretendard Variable
- **기본 아바타**: DiceBear `fun-emoji` 스타일 (`getDefaultAvatar()` in `utils.ts`)
- **아바타 리소스**: [DiceBear](https://www.dicebear.com/styles/) - 30+ 스타일, seed 기반 결정적 아바타 생성, API: `https://api.dicebear.com/9.x/{style}/svg?seed={seed}`
-- **상세 스펙**: `docs/UI-DESIGN-SYSTEM.md` 참조
+- **상세 스펙**: `docs/26-03-06-ui-design-system.md` 참조
## 에이전트 활용 가이드
@@ -153,11 +152,10 @@ pnpm --filter @blog-study/bot init-rounds # 회차 초기화
- `SUPABASE_SERVICE_KEY`, `DATABASE_URL`, `DATABASE_URL_DIRECT` (DB)
- `DISCORD_TOKEN`, `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`, `DISCORD_GUILD_ID`
- `ADMIN_DISCORD_IDS` (관리자 Discord ID, 쉼표 구분)
-- `OPENAI_API_KEY` (AI 기능용, 예정)
**env 파일 위치** (2곳):
-- `/Users/hansangho/Desktop/study-admin/.env.local` — 루트 (shared/bot용)
-- `/Users/hansangho/Desktop/study-admin/packages/web/.env.local` — Next.js용
+- `.env.local` — 루트 (shared/bot용)
+- `packages/web/.env.local` — Next.js용
**주의**: `packages/web/.env.local`에도 동일 환경변수 필요 (Next.js는 패키지 디렉토리 기준)
@@ -167,7 +165,7 @@ pnpm --filter @blog-study/bot init-rounds # 회차 초기화
```bash
cd packages/shared
-export $(grep DATABASE_URL /Users/hansangho/Desktop/study-admin/.env.local | head -1 | xargs)
+export $(grep DATABASE_URL ../../.env.local | head -1 | xargs)
npx drizzle-kit push --force
```
@@ -176,9 +174,16 @@ npx drizzle-kit push --force
| 문서 | 설명 |
|------|------|
| `docs/ARCHITECTURE.md` | 시스템 아키텍처 (Mermaid 다이어그램) |
-| `docs/TECH-DECISIONS.md` | 기술 선택 근거 (ADR) |
-| `docs/UI-DESIGN-SYSTEM.md` | UI 디자인 시스템 스펙 |
-| `docs/DEVELOPMENT.md` | 개발 환경 설정 |
-| `docs/CHECKLIST.md` | 구현 체크리스트 |
-| `docs/schema-summary.md` | DB 스키마 요약 (테이블/Enum/FK) |
-| `docs/patterns.md` | API 패턴 & 코드 규칙 |
+| `docs/26-03-06-tech-decisions.md` | 기술 선택 근거 (ADR) |
+| `docs/26-03-06-ui-design-system.md` | UI 디자인 시스템 스펙 |
+| `docs/26-03-06-development.md` | 개발 환경 설정 |
+| `docs/26-03-06-checklist.md` | 구현 체크리스트 |
+| `docs/26-03-06-schema-summary.md` | DB 스키마 요약 (테이블/Enum/FK) |
+| `docs/26-03-06-patterns.md` | API 패턴 & 코드 규칙 |
+
+## docs 파일명 컨벤션
+
+`yy-mm-dd-{설명}.md` — 예: `26-03-03-system-architecture.md`
+- 설명은 다른 문서와 구분될 정도로 구체적으로 작명
+- `docs/plans/` 하위도 동일 컨벤션 적용
+- 단, `docs/ARCHITECTURE.md`는 제외하며 업데이트 시에도 네이밍을 그대로 유지
diff --git a/README.md b/README.md
index 8054e19..39cb161 100644
--- a/README.md
+++ b/README.md
@@ -15,21 +15,20 @@ packages/
| 영역 | 기술 |
|------|------|
-| Frontend | Next.js 14, React 18, Tailwind CSS, shadcn/ui |
+| Frontend | Next.js 16, React 19, Tailwind CSS v4, shadcn/ui |
| Backend | Next.js API Routes, Supabase |
| Bot | discord.js v14, pg-boss |
-| DB | PostgreSQL (Supabase), Drizzle ORM, pgvector |
-| AI | OpenAI GPT-4o-mini, text-embedding-3-small |
-| Deploy | Vercel (Web), Railway (Bot), Supabase (DB) |
+| DB | PostgreSQL (Supabase), Drizzle ORM |
+| Deploy | Vercel (Web), AWS EC2 (Bot), Supabase (DB) |
## 주요 기능
- **RSS 자동 수집** — 블로그 글 발행 감지 및 수집
-- **AI 요약 & 추천** — GPT 기반 글 요약, 키워드 추출, 유사 글 추천
- **출석 자동화** — 2주 1회차, 지각/결석 자동 판정
- **벌금 관리** — 자동 부과, DM 알림, 납부 확인
-- **랭킹 & 통계** — 포스트 수, 출석률 기반 실시간 랭킹
-- **큐레이션** — 관심 키워드 기반 컨퍼런스/아티클 추천
+- **활동 점수 & 랭킹** — 포스트 수, 출석률, 활동 기반 실시간 랭킹
+- **큐레이션** — 태그 + 관심 키워드 기반 컨퍼런스/아티클 추천
+- **커뮤니티 게시판** — 공지/일반/자유 게시글 + 댓글
## 시작하기
@@ -57,7 +56,7 @@ pnpm typecheck
## 문서
- [아키텍처](docs/ARCHITECTURE.md)
-- [기술 결정](docs/TECH-DECISIONS.md)
-- [UI 디자인 시스템](docs/UI-DESIGN-SYSTEM.md)
-- [개발 환경](docs/DEVELOPMENT.md)
-- [구현 체크리스트](docs/CHECKLIST.md)
+- [기술 결정](docs/26-03-06-tech-decisions.md)
+- [UI 디자인 시스템](docs/26-03-06-ui-design-system.md)
+- [개발 환경](docs/26-03-06-development.md)
+- [구현 체크리스트](docs/26-03-06-checklist.md)
diff --git a/docs/26-03-06-checklist.md b/docs/26-03-06-checklist.md
new file mode 100644
index 0000000..4a73f7e
--- /dev/null
+++ b/docs/26-03-06-checklist.md
@@ -0,0 +1,128 @@
+# 구현 체크리스트
+
+전면 개편 작업 순서. 의존성 순서대로 정렬.
+
+---
+
+## Phase 0: 기반 정리 ✅
+
+- [x] 기존 `.kiro/`, `.vscode/`, 구 `docs/` 파일 제거
+- [x] 새 `CLAUDE.md` 및 `docs/` 문서 체계 확립
+- [x] Git 초기 커밋 (새 출발점)
+
+## Phase 3: 인증 전환 (Supabase Auth) ✅
+
+> Phase 1보다 먼저 진행됨 (인증이 모든 기능의 기반)
+
+- [x] Supabase 프로젝트에 Discord OAuth 프로바이더 설정
+- [x] `@supabase/ssr` 기반 클라이언트 설정 (`packages/web/src/lib/supabase/`)
+- [x] 로그인 페이지: "Discord로 로그인" 버튼으로 교체
+- [x] 회원가입 페이지 제거 (Discord OAuth로 통합)
+- [x] `middleware.ts` → Supabase Auth 세션 체크로 교체
+- [x] 관리자 권한: Discord ID + `ADMIN_DISCORD_IDS` 매칭
+- [x] 기존 auth 관련 파일/라우트 제거
+- [x] `api/auth/callback` 라우트 추가 (Supabase OAuth 콜백)
+- [x] 모든 API 라우트 Supabase Auth 기반으로 리팩토링
+- [x] DB 스키마에서 `users`, `sessions` 테이블 제거
+- [x] 보안 강화 (오픈 리다이렉트 방지, 입력 검증)
+- [x] 불필요한 의존성 제거: `bcryptjs`, `jsonwebtoken`, `jose`, `resend`
+- [x] DB 연결: Transaction Pooler 지원 (`prepare: false`)
+- [x] 빌드 검증 통과
+- [ ] RLS 정책 설정 (배포 시)
+
+## Phase 1: 의존성 업그레이드 ✅
+
+- [x] Next.js 14 → 16 업그레이드 (PR #5)
+- [x] Tailwind CSS v3 → v4 업그레이드 (PR #5)
+- [x] React 18 → 19 업그레이드 (PR #5)
+- [x] `rss-parser` → `feedsmith` 교체
+- [x] `node-cron` → `pg-boss` 교체
+- [x] 전체 빌드 확인 (`pnpm build`)
+
+## Phase 4: Bot 개편 (pg-boss + feedsmith) ✅
+
+- [x] `feedsmith` 기반 RSS 서비스 재구현
+- [x] `pg-boss` 기반 스케줄러 전환 (6개 잡)
+- [x] RSS→PostService.create→NotificationService 파이프라인 연결
+- [x] graceful shutdown에 pg-boss 정리 추가
+
+## Phase 6: 웹 UI 전면 리디자인
+
+### 6-1: 디자인 시스템 기반
+- [ ] globals.css 컬러 토큰 재정의 (스카이블루 포인트)
+- [x] Pretendard 폰트 설정
+- [x] Tailwind CSS v4 + 디자인 토큰 설정
+- [x] next-themes 다크모드 설정
+- [x] shadcn/ui 컴포넌트 테마 커스터마이징
+
+### 6-2: 레이아웃 ✅
+- [x] AppLayout (사이드바 + 헤더 + 메인)
+- [x] Sidebar (접기/펼치기, 활성 인디케이터, 아이콘 모드)
+- [x] 반응형: 모바일 드로어, 태블릿/데스크톱 사이드바
+
+### 6-3: Public 페이지
+- [ ] 랜딩 페이지 (/) - 스터디 소개, 로그인 유도
+- [x] 로그인 페이지 (/login) - Discord OAuth 버튼
+
+### 6-4: 사용자 페이지 ✅
+- [x] 대시보드 (/dashboard) - 현재 회차, 내 출석, 최근 글
+- [x] 글 목록 (/posts) - 스터디원 글 + 수동 등록 + 페이지네이션
+- [x] 랭킹 (/ranking) - 포디움, 정렬 (총점/포스트/활동), 출석 히트맵
+- [x] 큐레이션 (/curation) - 추천/최신 정렬, 카테고리/태그 필터, 무한 스크롤
+- [x] 게시판 (/board) - 카테고리별 게시글 + 댓글 + 비밀글 + Tiptap 에디터
+- [x] 멤버 목록 (/members) - 활동 멤버 그리드
+- [x] 멤버 프로필 (/members/[id]) - 상세 프로필 + 활동 통계
+- [x] 프로필 (/profile) - 내 정보 조회/수정
+- [x] 온보딩 (/profile/onboarding) - 최초 가입 설정
+
+### 6-5: 관리자 페이지
+- [x] 관리자 대시보드 (/admin) - 요약 통계, 활동 피드
+- [x] 멤버 관리 (/admin/members) - CRUD + 승인 + 상태 관리
+- [ ] 출석 관리 (/admin/attendance) - 멤버 × 회차 그리드 (검증 필요)
+- [ ] 벌금 관리 (/admin/fines) - 납부/면제 처리 (검증 필요)
+- [ ] 점수 관리 (/admin/scores) - 수동 부여/삭제 (검증 필요)
+- [x] 큐레이션 소스 (/admin/curation) - 소스 관리 + 크롤링
+- [ ] 설정 (/admin/settings) - 스터디 설정 (검증 필요)
+
+### 6-6: Supabase Realtime 통합 (선택적)
+- [ ] 활동 피드 컴포넌트 (실시간 업데이트)
+- [ ] 대시보드 통계 실시간 반영
+
+## Phase 7: 통합 테스트 및 배포
+
+- [ ] 전체 빌드 성공 확인
+- [ ] 타입 체크 통과
+- [ ] 린트 통과
+- [ ] Supabase 프로덕션 설정
+ - RLS 정책
+ - Discord OAuth 프로바이더
+- [ ] AWS EC2 배포 (Bot)
+ - 환경 변수 설정
+ - 빌드 확인
+- [ ] Vercel 배포 (Web)
+ - 환경 변수 설정
+ - Supabase URL/키 설정
+ - 빌드 확인
+- [ ] E2E 수동 테스트
+ - Discord 봇 커맨드 전체 동작
+ - 웹 로그인 플로우
+ - RSS 수집 → 알림 파이프라인
+ - 관리자 페이지 전체 기능
+
+---
+
+## 우선순위 요약
+
+```
+Phase 0 (정리) ✅
+ ↓
+Phase 3 (인증) ✅
+ ↓
+Phase 1 (의존성) ✅
+ ↓
+Phase 4 (봇) ✅
+ ↓
+Phase 6 (UI) ←── 관리자 페이지 검증 + 랜딩 + 디자인 정비 남음
+ ↓
+Phase 7 (배포)
+```
diff --git a/docs/26-03-06-development.md b/docs/26-03-06-development.md
new file mode 100644
index 0000000..410d510
--- /dev/null
+++ b/docs/26-03-06-development.md
@@ -0,0 +1,206 @@
+# 개발 환경 설정
+
+## 사전 요구사항
+
+- Node.js 22 이상
+- pnpm 8 이상 (`npm install -g pnpm`)
+- Discord 개발자 계정 ([discord.com/developers](https://discord.com/developers/applications))
+- Supabase 프로젝트 ([supabase.com](https://supabase.com))
+
+## 초기 설정
+
+```bash
+# 1. 의존성 설치
+pnpm install
+
+# 2. 환경 변수 설정
+cp .env.example .env.local
+cp .env.example packages/web/.env.local
+# 두 파일 모두 값을 채워야 합니다 (Next.js는 packages/web/ 기준으로 읽음)
+
+# 3. shared 패키지 빌드 (web/bot이 의존)
+pnpm --filter @blog-study/shared build
+
+# 4. DB 스키마 푸시
+DATABASE_URL="your-connection-string" pnpm --filter @blog-study/shared db:push
+
+# 5. 스터디 회차 초기화
+pnpm --filter @blog-study/bot init-rounds
+
+# 6. Discord 슬래시 커맨드 등록
+pnpm --filter @blog-study/bot deploy-commands
+```
+
+## 개발 서버
+
+```bash
+# 봇과 웹을 각각 별도 터미널에서 실행
+pnpm dev:bot # 봇 (tsx watch)
+pnpm dev:web # 웹 (localhost:3000)
+
+# 포트 지정
+pnpm --filter @blog-study/web dev --port 3200
+```
+
+## 환경 변수
+
+루트 `.env.local`과 `packages/web/.env.local` 두 곳에 동일하게 설정 필요.
+
+```env
+# Supabase
+NEXT_PUBLIC_SUPABASE_URL= # 프로젝트 URL (https://xxx.supabase.co)
+NEXT_PUBLIC_SUPABASE_ANON_KEY= # 클라이언트용 공개 키
+SUPABASE_SERVICE_KEY= # 서버용 비밀 키
+DATABASE_URL= # Transaction Pooler URL
+ # postgresql://postgres.xxx:[password]@aws-1-ap-northeast-2.pooler.supabase.com:6543/postgres
+
+# Discord
+DISCORD_TOKEN= # 봇 토큰
+DISCORD_CLIENT_ID= # 클라이언트 ID
+DISCORD_CLIENT_SECRET= # 클라이언트 시크릿 (OAuth용)
+DISCORD_GUILD_ID= # 서버(길드) ID
+
+# Admin
+ADMIN_DISCORD_IDS=id1,id2 # 관리자 Discord User ID (쉼표 구분)
+
+# Application
+APP_URL=http://localhost:3000 # 웹 URL
+NODE_ENV=development
+
+# Study Config
+STUDY_START_DATE=2024-01-01 # 스터디 시작일
+TOTAL_ROUNDS=10 # 총 회차 수
+```
+
+### 환경변수 주의사항
+
+- `NEXT_PUBLIC_` 접두사가 있는 변수만 브라우저에 노출됨
+- `packages/web/.env.local`이 없으면 Next.js가 환경변수를 읽지 못함
+- `DATABASE_URL`은 **Transaction Pooler** URL 사용 (Direct connection은 DNS 미지원 가능)
+
+## 테스트
+
+```bash
+# 전체 테스트
+pnpm test
+
+# 패키지별
+pnpm --filter @blog-study/bot test
+pnpm --filter @blog-study/shared test
+
+# 워치 모드
+pnpm --filter @blog-study/bot test:watch
+```
+
+## 빌드
+
+```bash
+# 전체 빌드 (shared → bot/web 순서)
+pnpm build
+
+# 패키지별
+pnpm build:bot # tsup으로 번들링 → dist/
+pnpm build:web # next build
+```
+
+## 코드 품질
+
+```bash
+pnpm lint # ESLint
+pnpm format # Prettier (자동 수정)
+pnpm format:check # Prettier (검사만)
+pnpm typecheck # TypeScript 타입 체크
+```
+
+## Supabase 설정
+
+### Discord OAuth 프로바이더
+1. Supabase Dashboard → Authentication → Providers → Discord
+2. `DISCORD_CLIENT_ID`와 `DISCORD_CLIENT_SECRET` 입력
+3. Callback URL을 Discord Developer Portal에 등록
+
+### Redirect URL 설정
+1. Supabase Dashboard → Authentication → URL Configuration
+2. Redirect URLs에 추가:
+ - `http://localhost:3000/auth/callback` (로컬)
+ - `http://localhost:3200/auth/callback` (로컬 대체 포트)
+ - `https://your-domain.vercel.app/auth/callback` (프로덕션)
+
+### pg-boss 설정
+pg-boss는 첫 실행 시 자동으로 필요한 테이블을 생성합니다.
+
+## Discord 봇 설정
+
+### 1. 봇 생성
+1. [Discord Developer Portal](https://discord.com/developers/applications) 접속
+2. "New Application" → 이름 입력
+3. "Bot" 탭 → "Add Bot"
+4. Token 복사 → `DISCORD_TOKEN`에 입력
+5. **Privileged Gateway Intents** 모두 활성화:
+ - PRESENCE INTENT
+ - SERVER MEMBERS INTENT
+ - MESSAGE CONTENT INTENT
+
+### 2. OAuth2 설정
+1. "OAuth2" → "General"
+2. Redirects에 Supabase 콜백 URL 추가
+3. Client Secret 복사 → `DISCORD_CLIENT_SECRET`에 입력
+
+### 3. 봇 초대
+1. "OAuth2" → "URL Generator"
+2. Scopes: `bot`, `applications.commands`
+3. Bot Permissions: `Send Messages`, `Manage Roles`, `Embed Links`, `Add Reactions`, `Use Slash Commands`
+4. 생성된 URL로 서버에 초대
+
+## 프로젝트 구조
+
+```
+study-admin/
+├── CLAUDE.md # Claude Code 프로젝트 설정
+├── .env.example # 환경 변수 템플릿
+├── .env.local # 로컬 환경 변수 (gitignored)
+├── package.json # 루트 패키지 (스크립트)
+├── pnpm-workspace.yaml # 모노레포 설정
+├── tsconfig.json # 공통 TypeScript 설정
+├── vercel.json # Vercel 배포 설정
+├── docs/ # 프로젝트 문서
+│ ├── ARCHITECTURE.md # 시스템 아키텍처
+│ ├── TECH-DECISIONS.md # 기술 선택 근거 (ADR)
+│ ├── UI-DESIGN-SYSTEM.md # UI 디자인 시스템
+│ ├── DEVELOPMENT.md # (이 파일)
+│ └── CHECKLIST.md # 구현 체크리스트
+└── packages/
+ ├── bot/ # Discord 봇
+ │ ├── src/
+ │ │ ├── index.ts # 엔트리포인트
+ │ │ ├── bot.ts # Discord 클라이언트
+ │ │ ├── commands/ # 슬래시 커맨드
+ │ │ ├── services/ # 비즈니스 로직
+ │ │ ├── schedulers/ # 크론/잡 큐
+ │ │ ├── handlers/ # 이벤트 핸들러
+ │ │ └── scripts/ # 초기화 스크립트
+ │ └── package.json
+ ├── web/ # Next.js 대시보드
+ │ ├── .env.local # 웹 전용 환경 변수 (gitignored)
+ │ ├── middleware.ts # 라우트 보호 미들웨어
+ │ ├── src/
+ │ │ ├── app/ # App Router 페이지
+ │ │ │ ├── auth/callback/ # OAuth 콜백
+ │ │ │ ├── (auth)/ # 로그인 페이지
+ │ │ │ ├── (user)/ # 사용자 페이지
+ │ │ │ └── (admin)/ # 관리자 페이지
+ │ │ ├── components/ # UI 컴포넌트 (shadcn/ui)
+ │ │ └── lib/ # 유틸리티
+ │ │ ├── supabase/ # Supabase 클라이언트 (client/server/middleware)
+ │ │ ├── admin.ts # 관리자 권한
+ │ │ ├── db.ts # DB 연결
+ │ │ └── api-error.ts # API 에러 처리
+ │ └── package.json
+ └── shared/ # 공유 코드
+ ├── src/
+ │ ├── db/ # Drizzle 스키마, 연결
+ │ ├── config/ # 설정
+ │ └── utils/ # 유틸리티 함수
+ ├── drizzle.config.ts # Drizzle Kit 설정
+ └── package.json
+```
diff --git a/docs/26-03-06-patterns.md b/docs/26-03-06-patterns.md
new file mode 100644
index 0000000..cc2e8c5
--- /dev/null
+++ b/docs/26-03-06-patterns.md
@@ -0,0 +1,232 @@
+# API 패턴 & 코드 규칙
+
+## 관리자 API 인증 패턴
+
+### withAdminAuth 래퍼 (권장)
+
+```ts
+// packages/web/src/lib/admin.ts 의 withAdminAuth 사용
+import { withAdminAuth } from '@/lib/admin';
+
+export const GET = withAdminAuth(async (request: NextRequest, adminAuth) => {
+ // adminAuth.discordId — 관리자의 Discord ID
+ // adminAuth.userId — Supabase user ID
+ const database = db();
+ // ... DB 조회 로직
+ return NextResponse.json({ data });
+});
+```
+
+### 인증 흐름
+```
+createClient() → getUser() → identities[].find(p => p.provider === 'discord').id
+→ isAdminDiscordId(discordId) → 관리자 확인
+```
+
+### 관련 파일
+| 파일 | 역할 |
+|------|------|
+| `packages/web/src/lib/admin.ts` | `withAdminAuth`, `verifyAdminAccess`, `isAdminDiscordId` |
+| `packages/web/src/lib/supabase/server.ts` | `createClient()` — 서버용 Supabase |
+| `packages/web/src/lib/supabase/client.ts` | 브라우저용 Supabase |
+| `packages/web/src/lib/db.ts` | `db()` — shared DB 인스턴스 래퍼 |
+
+## Drizzle ORM Import 패턴
+
+```ts
+// shared 패키지에서 스키마 + enum import
+import { db as sharedDb } from '@blog-study/shared';
+const { members, posts, attendance, MemberStatus, AttendanceStatus } = sharedDb;
+
+// drizzle-orm 연산자
+import { eq, count, sql, asc, desc, and, or, inArray } from 'drizzle-orm';
+
+// DB 인스턴스 (web 패키지)
+import { db } from '@/lib/db';
+const database = db();
+```
+
+## API Route 기본 구조 (Next.js App Router)
+
+```ts
+// packages/web/src/app/api/admin/{resource}/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { eq } from 'drizzle-orm';
+import { db } from '@/lib/db';
+import { db as sharedDb } from '@blog-study/shared';
+import { withAdminAuth } from '@/lib/admin';
+
+const { members, MemberStatus } = sharedDb;
+
+// GET — 목록 조회
+export const GET = withAdminAuth(async (request: NextRequest, _adminAuth) => {
+ try {
+ const { searchParams } = new URL(request.url);
+ const database = db();
+ const result = await database.select().from(members);
+ return NextResponse.json({ data: result });
+ } catch (error) {
+ console.error('API error:', error);
+ return NextResponse.json({ message: '서버 오류' }, { status: 500 });
+ }
+});
+
+// POST — 생성
+export const POST = withAdminAuth(async (request: NextRequest, _adminAuth) => {
+ try {
+ const body = await request.json();
+ // validation → insert → returning
+ return NextResponse.json({ message: '성공', data: newItem });
+ } catch (error) {
+ return NextResponse.json({ message: '서버 오류' }, { status: 500 });
+ }
+});
+```
+
+### 동적 라우트 (패턴: `/api/admin/{resource}/[id]/route.ts`)
+
+```ts
+// PATCH — 수정
+export const PATCH = withAdminAuth(async (request: NextRequest, _adminAuth) => {
+ const id = request.url.split('/').pop(); // 또는 params에서 추출
+ // update → returning
+});
+
+// DELETE — 삭제
+export const DELETE = withAdminAuth(async (request: NextRequest, _adminAuth) => {
+ const id = request.url.split('/').pop();
+ // delete → returning
+});
+```
+
+## 일반 사용자 API 패턴
+
+```ts
+// 인증만 필요, 관리자 권한 불필요
+import { createClient } from '@/lib/supabase/server';
+
+export async function GET() {
+ const supabase = await createClient();
+ const { data: { user } } = await supabase.auth.getUser();
+ if (!user) return NextResponse.json({ error: '인증 필요' }, { status: 401 });
+
+ const discordId = user.identities?.find(i => i.provider === 'discord')?.id;
+ // discordId로 members 테이블 조회
+}
+```
+
+## 멤버 상태 리다이렉트 패턴
+
+서버 + 클라이언트 이중 체크로 차단 상태 사용자의 접근을 제어:
+
+### 1. OAuth 콜백 (서버 사이드)
+
+```ts
+// packages/web/src/app/auth/callback/route.ts
+// 로그인 직후 DB에서 status 조회 → 상태별 리다이렉트
+if (!memberData || !memberData.onboardingCompleted) → /profile/onboarding
+if (memberData.status === 'pending_approval') → /pending
+if (memberData.status === 'inactive') → /inactive
+```
+
+### 2. UserLayout (클라이언트 사이드)
+
+```ts
+// packages/web/src/app/(user)/layout.tsx
+// checkedPathname 패턴: pathname 변경 시 자동으로 로딩 상태 진입 (플래시 방지)
+const [checkedPathname, setCheckedPathname] = useState(null);
+
+// /api/auth/me 응답의 status 필드로 리다이렉트 판단
+// 차단 페이지 자체는 예외 처리: blockedPages = ['/pending', '/inactive']
+
+// 로딩 가드: checkedPathname !== pathname이면 로딩 표시
+if (checkedPathname !== pathname && pathname !== '/profile/onboarding') → 로딩
+```
+
+### 주의사항
+- `(admin)` 레이아웃은 별도 인증 → 멤버 상태 체크 없음 (관리자가 스스로 승인 가능)
+- `pending`/`inactive` 페이지는 `(user)` 그룹 내에 있지만 리다이렉트 예외 처리됨
+
+## 다이얼로그 패턴
+
+`window.confirm()`/`window.alert()`/`window.prompt()` 사용 금지. 커스텀 다이얼로그 사용:
+
+```tsx
+// AlertDialog (shadcn/ui) 패턴 — DeletePostDialog 참고
+
+
+
+ 제목
+ 설명
+
+
+ 취소
+ 확인
+
+
+
+```
+
+## 게시판 API 인증 패턴
+
+관리자 전용이 아닌 일반 사용자 API는 `getBoardAuth` + `successResponse`/`Errors` 조합 사용:
+
+```ts
+// packages/web/src/lib/board-auth.ts
+import { getBoardAuth } from '@/lib/board-auth';
+import { successResponse, errorResponse, Errors } from '@/lib/api-error';
+
+export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+ try {
+ const auth = await getBoardAuth();
+ if (!auth) return Errors.unauthorized().toResponse();
+
+ const { id } = await params;
+ const database = getDb();
+ // ... DB 조회
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error);
+ }
+}
+```
+
+### getBoardAuth vs withAdminAuth
+| 함수 | 용도 | 반환 |
+|------|------|------|
+| `withAdminAuth` | 관리자 전용 API 래퍼 | `adminAuth.discordId`, `adminAuth.userId` |
+| `getBoardAuth` | 일반 사용자 API 함수 | `{ memberId, discordId, isAdmin }` 또는 `null` |
+
+### API 표준 응답 (`api-error.ts`)
+| 함수 | 용도 |
+|------|------|
+| `successResponse(data, message?)` | `{ success: true, data, message }` |
+| `errorResponse(error)` | ApiError → 표준 에러 응답, unknown → 500 |
+| `Errors.unauthorized()` | 401 |
+| `Errors.forbidden(msg)` | 403 |
+| `Errors.notFound(msg)` | 404 |
+| `Errors.badRequest(msg)` | 400 |
+
+## MemberAvatar 재사용 컴포넌트
+
+프로필 아바타 + 이름 + 멤버 상세 링크 + 관리자 뱃지를 통합 제공:
+
+```tsx
+import { MemberAvatar } from '@/components/ui/member-avatar';
+
+// 기본: 아바타만 (클릭 시 멤버 상세로 이동)
+
+
+// 아바타 + 이름 + 관리자 뱃지
+
+
+// 링크 비활성화 (삭제된 댓글 등)
+
+```
+
+| Prop | 타입 | 설명 |
+|------|------|------|
+| `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | 아바타 크기 |
+| `showName` | boolean | 이름 표시 + 링크 포함 |
+| `noLink` | boolean | 링크 비활성화 |
+| `isAdmin` | boolean | 관리자 뱃지 표시 |
diff --git a/docs/26-03-06-schema-summary.md b/docs/26-03-06-schema-summary.md
new file mode 100644
index 0000000..e008739
--- /dev/null
+++ b/docs/26-03-06-schema-summary.md
@@ -0,0 +1,159 @@
+# DB 스키마 요약
+
+> 소스: `packages/shared/src/db/schema.ts`
+
+## Enum 타입
+
+| Enum | 값 | 용도 |
+|------|-----|------|
+| `MemberStatus` | `pending_approval`, `active`, `inactive`, `dormant`, `ob`, `withdrawn` | 멤버 상태 |
+| `AttendanceStatus` | `pending`, `submitted`, `late`, `absent` | 출석 상태 |
+| `FineType` | `late`, `absent` | 벌금 사유 |
+| `FineStatus` | `unpaid`, `paid`, `waived` | 벌금 납부 |
+| `CurationCategory` | `conference`, `article` | 큐레이션 분류 |
+| `ActivityScoreType` | `blog_post`, `discord_message`, `discord_thread`, `discord_reaction`, `admin_manual`, `post_view` | 활동 점수 |
+| `BoardCategory` | `notice`, `suggestion`, `review`, `knowledge`, `daily`, `etc` | 게시판 카테고리 (const object) |
+
+## 테이블
+
+### members
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | defaultRandom |
+| `discord_id` | varchar(20) | unique, not null |
+| `discord_username` | varchar(100) | not null |
+| `name` | varchar(50) | not null |
+| `nickname` | varchar(100) | not null |
+| `part` | varchar(50) | not null |
+| `blog_url` | varchar(500) | not null |
+| `rss_url` | varchar(500) | nullable |
+| `profile_image_url` | varchar(500) | nullable |
+| `bio` | varchar(200) | nullable |
+| `interests` | text[] | nullable |
+| `resolution` | varchar(300) | nullable |
+| `onboarding_completed` | boolean | default false |
+| `rss_consent` | boolean | default true |
+| `github_url`, `linkedin_url`, `instagram_url` | varchar(500) | 소셜 링크 |
+| `status` | varchar(20) | default 'active' |
+| `dormant_start_round` | integer | nullable |
+| `dormant_used` | boolean | default false |
+| `joined_at`, `updated_at` | timestamptz | defaultNow |
+
+### rounds
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | serial PK | |
+| `round_number` | integer | unique, not null |
+| `start_date`, `end_date`, `grace_end_date` | date | not null |
+| `is_current` | boolean | default false |
+
+### posts
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | |
+| `member_id` | uuid FK → members | not null |
+| `round_id` | integer FK → rounds | nullable |
+| `title` | varchar(500) | not null |
+| `url` | varchar(1000) | unique, not null |
+| `published_at` | timestamptz | not null |
+| `description` | text | nullable |
+| `collected_at` | timestamptz | defaultNow |
+
+### attendance
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | |
+| `member_id` | uuid FK → members | not null |
+| `round_id` | integer FK → rounds | not null |
+| `status` | varchar(20) | default 'pending' |
+| `submitted_at` | timestamptz | nullable |
+| unique constraint: `(member_id, round_id)` |
+
+### fines
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | |
+| `member_id` | uuid FK → members | not null |
+| `round_id` | integer FK → rounds | not null |
+| `type` | varchar(20) | late/absent |
+| `amount` | integer | not null |
+| `status` | varchar(20) | default 'unpaid' |
+| unique constraint: `(member_id, round_id)` |
+
+### activity_scores
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | |
+| `member_id` | uuid FK → members | not null |
+| `type` | varchar(30) | ActivityScoreType |
+| `points` | integer | not null |
+| `description` | varchar(300) | nullable |
+| `date` | date | 일일 상한 체크용 |
+
+### post_views
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | |
+| `member_id` | uuid FK → members | not null |
+| `post_id` | uuid FK → posts | not null |
+| unique constraint: `(member_id, post_id)` |
+
+### config
+`key` (varchar PK) / `value` (text) / `updated_at` — 설정 키-값 저장소
+
+### board_posts
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | defaultRandom |
+| `member_id` | uuid FK → members | not null |
+| `category` | varchar(20) | BoardCategory, not null |
+| `title` | varchar(200) | not null |
+| `content` | jsonb | Tiptap JSON, not null |
+| `content_text` | text | 검색용 평문, not null |
+| `is_secret` | boolean | default false |
+| `is_pinned` | boolean | default false |
+| `comment_count` | integer | default 0 |
+| `created_at`, `updated_at` | timestamptz | defaultNow |
+| `deleted_at` | timestamptz | nullable (soft delete) |
+| 인덱스: `member_id`, `category`, `is_pinned`, `created_at` |
+
+### board_comments
+| 컬럼 | 타입 | 비고 |
+|------|------|------|
+| `id` | uuid PK | defaultRandom |
+| `post_id` | uuid FK → board_posts | not null |
+| `member_id` | uuid FK → members | not null |
+| `parent_id` | uuid FK → board_comments | nullable (대댓글) |
+| `content` | text | not null |
+| `is_secret` | boolean | default false |
+| `created_at`, `updated_at` | timestamptz | defaultNow |
+| `deleted_at` | timestamptz | nullable (soft delete) |
+| 인덱스: `post_id`, `member_id`, `parent_id` |
+
+### keywords, curation_sources, curation_items
+- `keywords`: keyword(unique) + frequency + last_updated
+- `curation_sources`: url(unique) + name + category + rss_url + tags[] + is_active
+- `curation_items`: source_id FK → curation_sources, url(unique) + title + description + thumbnail_url + category + tags[] + relevance_score + is_shared
+
+## FK 관계 요약
+
+```
+members ──< posts (member_id)
+members ──< attendance (member_id)
+members ──< fines (member_id)
+members ──< activity_scores (member_id)
+members ──< post_views (member_id)
+rounds ──< posts (round_id)
+rounds ──< attendance (round_id)
+rounds ──< fines (round_id)
+posts ──< post_views (post_id)
+curation_sources ──< curation_items (source_id)
+members ──< board_posts (member_id)
+members ──< board_comments (member_id)
+board_posts ──< board_comments (post_id)
+board_comments ──< board_comments (parent_id, self-ref)
+```
+
+## 타입 Export
+
+모든 테이블에 `Type`/`NewType` export 있음 (예: `Member`/`NewMember`, `Post`/`NewPost`, `BoardPost`/`NewBoardPost`, `BoardComment`/`NewBoardComment`)
diff --git a/docs/26-03-06-tech-decisions.md b/docs/26-03-06-tech-decisions.md
new file mode 100644
index 0000000..6b2ccd5
--- /dev/null
+++ b/docs/26-03-06-tech-decisions.md
@@ -0,0 +1,121 @@
+# 기술 결정 기록 (ADR)
+
+## 결정 요약
+
+| # | 결정 | 선택 | 대안 | 이유 |
+|---|------|------|------|------|
+| 1 | Next.js 버전 | **16** | 14 유지, 15 | React 19, Tailwind v4 네이티브 지원 |
+| 2 | 인증 | **Supabase Auth** | 커스텀 JWT | Discord OAuth 네이티브, 유지보수 비용 제거 |
+| 3 | RSS 파서 | **feedsmith** | rss-parser | rss-parser 3년 미유지보수, feedsmith 활발 |
+| 4 | 작업 큐 | **pg-boss** | node-cron, Upstash Redis | 추가 인프라 불필요, PostgreSQL 기반 트랜잭션 |
+| 5 | 실시간 | **Supabase Realtime** (선택적) | WebSocket 직접 구현 | 활동 피드만 적용, 20줄로 구현 가능 |
+
+---
+
+## ADR-1: Next.js 16 업그레이드
+
+**상태**: ✅ 구현 완료 (PR #5)
+
+**결정**: Next.js 14 → 16 업그레이드 (React 19, Tailwind CSS v4 포함)
+
+**근거**:
+- React 19: 서버 컴포넌트/액션 안정화
+- Tailwind CSS v4: 네이티브 CSS 기반, 빌드 성능 향상
+- Turbopack 안정화: 빌드/HMR 성능 개선
+
+**마이그레이션 내용**:
+- `params`/`cookies()`/`headers()` async 변환
+- Tailwind v4 설정 마이그레이션 (PostCSS 기반)
+- React 18 → 19 호환성 업데이트
+
+---
+
+## ADR-2: Supabase Auth로 전환
+
+**상태**: ✅ 구현 완료 (PR #2)
+
+**결정**: 커스텀 이메일/JWT 인증 → Supabase Auth (Discord OAuth)
+
+**근거**:
+- Discord OAuth2 네이티브 지원 (콜백 URL 자동 관리)
+- JWT, 세션, 토큰 리프레시 코드 전부 삭제 가능
+- RLS 정책 활용 가능
+- 무료 50,000 MAU (스터디 규모에 충분)
+
+**아키텍처 분리**:
+- 웹: Supabase Auth (Discord OAuth2 PKCE 플로우)
+- 봇: `service_role` 키 + Discord ID로 직접 DB 조회/수정
+- 봇은 Auth 레이어 무관
+
+**구현 내용**:
+- `@supabase/ssr` 기반 SSR 클라이언트 (client/server/middleware)
+- Discord ID 추출: `user.identities[].id` where `provider === 'discord'`
+- 미들웨어: `updateSession()`으로 세션 자동 갱신 + 라우트 보호
+- 관리자: `ADMIN_DISCORD_IDS` 환경변수로 Discord ID 기반 권한 체크
+- DB: Transaction Pooler 사용 (`prepare: false`)
+
+**제거 완료**:
+- `users`, `sessions` 테이블 (Supabase Auth의 `auth.users`로 대체)
+- `packages/web/src/lib/auth.ts` (커스텀 JWT 로직)
+- `packages/web/src/lib/email.ts` (이메일 인증)
+- `bcryptjs`, `jsonwebtoken`, `jose`, `resend` 의존성
+- 회원가입/이메일인증 페이지 및 API
+
+---
+
+## ADR-3: feedsmith 채택
+
+**상태**: ✅ 구현 완료
+
+**결정**: rss-parser → feedsmith
+
+**근거**:
+- rss-parser: 마지막 배포 3년 전, 비활성
+- feedsmith: 2025 활발, RSS/Atom/RDF/JSON Feed 모두 지원
+- 피드 생성 기능도 있어 향후 RSS 피드 노출 가능
+- rss-parser보다 빠름
+
+**구현 내용**:
+- `RssService`: axios fetch + `parseFeed()` 분리 구조
+- `extractFeedItems()` 헬퍼로 RSS/Atom/JSON/RDF 전체 포맷 정규화
+- `detectRssUrl()`, `fetchFeed()` 모두 feedsmith 기반으로 전환
+
+---
+
+## ADR-4: pg-boss로 작업 큐 전환
+
+**상태**: ✅ 구현 완료
+
+**결정**: node-cron → pg-boss
+
+**근거**:
+- node-cron: 프로세스 내 메모리 기반, 재시작 시 상태 유실
+- pg-boss: PostgreSQL 기반, 트랜잭션 보장, 재시도/동시성 관리
+- Upstash Redis 불필요: 추가 인프라, 커맨드당 과금, BullMQ 호환 문제
+
+**구현 내용**:
+- `job-queue.ts`: pg-boss 싱글톤 (start/stop/get)
+- `scheduler-registry.ts`: 6개 cron 잡 등록 + 워커 연결
+- `index.ts`에서 pg-boss 시작 → 잡 등록 → graceful shutdown 통합
+- `DATABASE_URL_DIRECT` 환경변수 추가 (pg-boss LISTEN/NOTIFY용)
+
+**등록된 잡**:
+| 잡 이름 | cron | 핸들러 |
+|---------|------|--------|
+| `rss-poll` | `*/5 * * * *` | `RssPoller.poll()` |
+| `attendance-check` | `0 0 * * 2` | `AttendanceChecker.check()` |
+| `fine-reminder` | `0 10 * * *` | `FineReminder.sendReminders()` |
+| `round-report` | `5 0 * * 2` | `RoundReporter.sendRoundReport()` |
+| `curation-crawl` | `0 9 * * *` | `CurationCrawler.crawl()` |
+| `curation-share` | `0 10 * * *` | `CurationCrawler.shareDailyContent()` |
+
+---
+
+## 비용 총 요약
+
+| 서비스 | 월 비용 |
+|--------|--------|
+| AWS EC2 (Bot) | 기존 서버 활용 |
+| Vercel (Web) | $0 (무료) |
+| Supabase (DB + Auth) | $0 (무료) ~ $25 (Pro) |
+| **합계** | **$0 ~ $25/월** |
diff --git a/docs/26-03-06-ui-design-system.md b/docs/26-03-06-ui-design-system.md
new file mode 100644
index 0000000..f6bb301
--- /dev/null
+++ b/docs/26-03-06-ui-design-system.md
@@ -0,0 +1,286 @@
+# UI 디자인 시스템
+
+## 디자인 방향
+
+**컨셉**: Vercel 화이트/블랙 미니멀 + 스카이블루 포인트
+- 해외 어드민 사이트 느낌: 깔끔, 세련, 여백 활용
+- 참고: hazel-admin (레이아웃/컴포넌트), obsidian-quartz-blog (타이포/색상 체계)
+
+---
+
+## 컬러 시스템
+
+### Light Mode
+
+| 토큰 | 색상 | 용도 |
+|------|------|------|
+| `--background` | `#ffffff` | 페이지 배경 |
+| `--foreground` | `#18181b` (zinc-900) | 기본 텍스트 |
+| `--muted` | `#f4f4f5` (zinc-100) | 비활성 배경, 카드 배경 |
+| `--muted-foreground` | `#71717a` (zinc-500) | 보조 텍스트, 플레이스홀더 |
+| `--border` | `#e4e4e7` (zinc-200) | 테두리, 구분선 |
+| `--primary` | `#0ea5e9` (sky-500) | 주요 액션, 링크, 포인트 |
+| `--primary-hover` | `#0284c7` (sky-600) | 호버 상태 |
+| `--primary-foreground` | `#ffffff` | primary 위 텍스트 |
+| `--primary-muted` | `#e0f2fe` (sky-100) | 포인트 배경, 뱃지 |
+| `--secondary` | `#f4f4f5` (zinc-100) | 보조 버튼 배경 |
+| `--secondary-foreground` | `#18181b` | 보조 버튼 텍스트 |
+| `--destructive` | `#ef4444` (red-500) | 삭제, 에러 |
+| `--success` | `#22c55e` (green-500) | 성공, 출석 |
+| `--warning` | `#f59e0b` (amber-500) | 경고, 지각 |
+| `--accent` | `#f4f4f5` | 사이드바 호버 |
+| `--ring` | `#0ea5e9` | 포커스 링 |
+
+### Dark Mode
+
+| 토큰 | 색상 |
+|------|------|
+| `--background` | `#09090b` (zinc-950) |
+| `--foreground` | `#fafafa` (zinc-50) |
+| `--muted` | `#27272a` (zinc-800) |
+| `--muted-foreground` | `#a1a1aa` (zinc-400) |
+| `--border` | `#3f3f46` (zinc-700) |
+| `--primary` | `#38bdf8` (sky-400) |
+| `--primary-hover` | `#0ea5e9` (sky-500) |
+| `--primary-muted` | `#0c4a6e` (sky-900) |
+| `--secondary` | `#27272a` (zinc-800) |
+
+### 차트 컬러
+
+```
+--chart-1: #0ea5e9 (sky - 출석/제출)
+--chart-2: #22c55e (green - 성공)
+--chart-3: #f59e0b (amber - 지각)
+--chart-4: #ef4444 (red - 결석)
+--chart-5: #8b5cf6 (violet - 기타)
+```
+
+---
+
+## 타이포그래피
+
+### 폰트
+
+```css
+--font-sans: "Pretendard Variable", "Pretendard",
+ -apple-system, BlinkMacSystemFont, system-ui, Roboto,
+ "Helvetica Neue", "Segoe UI", sans-serif;
+
+--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular,
+ Menlo, Monaco, Consolas, monospace;
+```
+
+**Pretendard 로딩**: CDN (`cdn.jsdelivr.net/gh/orioncactus/pretendard`)
+
+### 크기 스케일
+
+| 용도 | 크기 | 무게 | 행간 |
+|------|------|------|------|
+| h1 (페이지 제목) | `clamp(1.65rem, 1.4rem + 1vw, 2rem)` | 700 | 1.2 |
+| h2 (섹션 제목) | `clamp(1.35rem, 1.2rem + 0.6vw, 1.5rem)` | 600 | 1.3 |
+| h3 (서브섹션) | `clamp(1.1rem, 1rem + 0.4vw, 1.25rem)` | 600 | 1.4 |
+| body | `0.875rem` (14px) | 400 | 1.6 |
+| small/label | `0.75rem` (12px) | 500 | 1.4 |
+| code | `0.85rem` | 400 | 1.5 |
+
+### 자간
+
+```css
+body { letter-spacing: -0.014em; }
+h1, h2, h3 { letter-spacing: -0.025em; }
+```
+
+---
+
+## 디자인 토큰
+
+### 간격 (Spacing)
+
+```css
+--spacing-xs: 0.25rem; /* 4px */
+--spacing-sm: 0.5rem; /* 8px */
+--spacing-md: 1rem; /* 16px */
+--spacing-lg: 1.5rem; /* 24px */
+--spacing-xl: 2rem; /* 32px */
+--spacing-2xl: 3rem; /* 48px */
+```
+
+### 라운딩 (Border Radius)
+
+```css
+--radius: 0.75rem; /* 12px - 기본 */
+--radius-sm: 0.5rem; /* 8px - 뱃지, 인라인 코드 */
+--radius-md: 0.625rem; /* 10px */
+--radius-lg: 0.75rem; /* 12px - 카드 */
+--radius-xl: 1rem; /* 16px - 모달 */
+--radius-full: 9999px; /* 원형 - 아바타 */
+```
+
+### 그림자 (Shadow)
+
+```css
+--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07),
+ 0 2px 4px -2px rgba(0, 0, 0, 0.05);
+--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08),
+ 0 4px 6px -4px rgba(0, 0, 0, 0.04);
+```
+
+### 트랜지션
+
+```css
+--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+--transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+```
+
+---
+
+## 레이아웃
+
+### 데스크톱 (≥1024px)
+
+```
+┌──────────────────────────────────────────────────────────┐
+│ Sidebar (240px) │ Main Content (max-w-7xl, mx-auto) │
+│ │ │
+│ ┌─────────────┐ │ ┌─ Header (sticky) ──────────────┐ │
+│ │ Logo │ │ │ 페이지 제목 [🔔] [🌙] [👤] │ │
+│ │ │ │ └────────────────────────────────┘ │
+│ │ ─────────── │ │ │
+│ │ 대시보드 │ │ ┌─ Content ──────────────────────┐ │
+│ │ 글 목록 │ │ │ │ │
+│ │ 랭킹 │ │ │ p-4 sm:p-6 lg:p-8 │ │
+│ │ 큐레이션 │ │ │ │ │
+│ │ │ │ └────────────────────────────────┘ │
+│ │ ─────────── │ │ │
+│ │ 관리자 │ │ │
+│ │ 멤버 │ │ │
+│ │ 출석 │ │ │
+│ │ 벌금 │ │ │
+│ │ 설정 │ │ │
+│ │ │ │ │
+│ │ ─────────── │ │ │
+│ │ [👤 유저] │ │ │
+│ └─────────────┘ │ │
+└──────────────────────────────────────────────────────────┘
+```
+
+### 모바일 (<1024px)
+
+- 사이드바 → 햄버거 메뉴 (오버레이 드로어)
+- 컨텐츠 전체 너비
+- 반응형 패딩: `p-4`
+
+### 사이드바 기능
+
+- 접기/펼치기 (아이콘만 모드)
+- localStorage로 상태 유지
+- 활성 메뉴: 좌측 스카이블루 바 인디케이터
+- 접힌 상태: 아이콘 + 툴팁
+
+---
+
+## 컴포넌트 패턴
+
+### 버튼 Variants
+
+| Variant | 배경 | 텍스트 | 용도 |
+|---------|------|--------|------|
+| `default` | zinc-900 | white | 주요 액션 |
+| `primary` | sky-500 | white | 포인트 액션 (참가, 저장) |
+| `secondary` | zinc-100 | zinc-900 | 보조 액션 |
+| `outline` | transparent + border | zinc-900 | 3차 액션 |
+| `ghost` | transparent | zinc-900 | 사이드바, 아이콘 |
+| `destructive` | red-500 | white | 삭제, 위험 액션 |
+| `link` | transparent | sky-500 | 텍스트 링크 |
+
+**크기**: `sm` (h-8), `default` (h-9), `lg` (h-10), `icon` (h-9 w-9)
+
+### 카드
+
+```
+┌─ Card ──────────────────────────────┐
+│ ┌─ CardHeader ──────────────────┐ │
+│ │ CardTitle CardAction │ │
+│ │ CardDescription │ │
+│ └──────────────────────────────┘ │
+│ ┌─ CardContent ─────────────────┐ │
+│ │ │ │
+│ └──────────────────────────────┘ │
+│ ┌─ CardFooter ──────────────────┐ │
+│ │ │ │
+│ └──────────────────────────────┘ │
+└────────────────────────────────────┘
+```
+
+- 패딩: `px-6`
+- 라운딩: `rounded-lg` (12px)
+- 호버: `translateY(-2px)` + 그림자 증가
+
+### 데이터 테이블
+
+- shadcn/ui Table 컴포넌트
+- 정렬, 필터, 페이지네이션
+- 모바일: 수직 레이아웃 전환
+
+### 상태 뱃지
+
+| 상태 | 색상 | 텍스트 |
+|------|------|--------|
+| 출석(submitted) | green-100/green-800 | 제출 완료 |
+| 미제출(pending) | zinc-100/zinc-600 | 미제출 |
+| 지각(late) | amber-100/amber-800 | 지각 |
+| 결석(absent) | red-100/red-800 | 결석 |
+| 활성(active) | sky-100/sky-800 | 활성 |
+| 휴면(dormant) | zinc-100/zinc-600 | 휴면 |
+| 탈퇴(withdrawn) | zinc-50/zinc-400 | 탈퇴 |
+
+---
+
+## 아이콘
+
+**lucide-react** 사용. 주요 아이콘:
+
+| 메뉴 | 아이콘 |
+|------|--------|
+| 대시보드 | `LayoutDashboard` |
+| 글 목록 | `FileText` |
+| 랭킹 | `Trophy` |
+| 큐레이션 | `Newspaper` |
+| 멤버 관리 | `Users` |
+| 출석 | `CalendarCheck` |
+| 벌금 | `Banknote` |
+| 설정 | `Settings` |
+| 알림 | `Bell` |
+| 다크모드 | `Moon` / `Sun` |
+| 프로필 | `User` |
+
+---
+
+## 다크모드
+
+- `next-themes` 사용
+- `attribute="class"` (html에 `dark` 클래스 추가)
+- `defaultTheme="system"` (시스템 설정 따름)
+- CSS 변수 기반 전환 (트랜지션 없음 - `disableTransitionOnChange`)
+
+---
+
+## 반응형 브레이크포인트
+
+| 이름 | 너비 | 변경 |
+|------|------|------|
+| mobile | < 768px | 1열, 사이드바 숨김 |
+| tablet | 768px - 1023px | 1열, 축소된 사이드바 |
+| desktop | ≥ 1024px | 사이드바 + 메인 컨텐츠 |
+
+---
+
+## 접근성 (a11y)
+
+- 시맨틱 HTML: ``, `
@@ -151,7 +151,7 @@ export default function Home() {
{/* ------------------------------------------------------------------ */}