판교 이노밸리 구내식당 주간 식단표를 카카오 채널에서 크롤링하여 Slack으로 자동 발송하는 봇
- 매주 월요일 오전 9시(KST)에 카카오 채널에서 새 식단표 크롤링
- 새 식단표 발견 시 지정된 Slack 채널로 자동 발송
- 발송 실패 시 최대 6회까지 1시간 간격으로 재시도
/식단입력 시 이번 주 식단표 즉시 조회- 캐시된 데이터 우선 반환, 없으면 실시간 크롤링
DeliveryHistory테이블로 채널별 발송 이력 관리- 동일 식단표를 같은 채널에 중복 발송하지 않음
┌─────────────────────────────────────────────────────────────────┐
│ Interface Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ HTTP Routes │ │ Slack Cmds │ │ Cron Scheduler │ │
│ │ /health │ │ /식단 │ │ 매주 월 09:00 KST │ │
│ │ /api/menu │ │ │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └────────────┬────────────┘ │
└─────────┼────────────────┼──────────────────────┼───────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ GetCurrentMenu │ │ CrawlWeeklyMenu │ │ CheckAndSendMenu│ │
│ │ UseCase │ │ UseCase │ │ UseCase │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
└───────────┼────────────────────┼────────────────────┼───────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Playwright │ │ Slack Bot │ │ Prisma Repository │ │
│ │ Crawler │ │ Service │ │ (SQLite) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
[카카오 채널] [Slack API] [SQLite DB]
| 분류 | 기술 | 용도 |
|---|---|---|
| Runtime | Node.js 20 | 서버 런타임 |
| Language | TypeScript 5 | 타입 안정성 |
| 크롤링 | Playwright | 카카오 채널 크롤링 (headless Chrome) |
| Slack | @slack/bolt | Socket Mode 기반 봇 |
| ORM | Prisma | SQLite 데이터베이스 |
| DI | TSyringe | 의존성 주입 컨테이너 |
| 스케줄링 | node-cron | 주간 자동 발송 |
| 컨테이너 | Docker | 배포 환경 |
- Node.js 20 이상
- Docker & Docker Compose (배포 시)
- Slack App (Bot Token, Signing Secret, App Token)
# 1. 저장소 클론
git clone https://github.com/kilhyeonjun/innovalley-menu-bot.git
cd innovalley-menu-bot
# 2. 의존성 설치
npm install
# 3. Playwright 브라우저 설치 (크롤링용)
npx playwright install chromium
# 4. 환경변수 설정
cp .env.example .env
# .env 파일을 열어 Slack 토큰 등 입력
# 5. Prisma 클라이언트 생성
npm run prisma:generate
# 6. 데이터베이스 마이그레이션
npm run prisma:migrate
# 7. 개발 서버 실행
npm run dev.env 파일에 다음 값들을 설정합니다:
# ─────────────────────────────────────────
# Database
# ─────────────────────────────────────────
DATABASE_URL="file:./data/menu.db"
# ─────────────────────────────────────────
# Slack Bot (필수)
# ─────────────────────────────────────────
# Bot User OAuth Token (xoxb-로 시작)
# Slack App > OAuth & Permissions > Bot User OAuth Token
SLACK_BOT_TOKEN=xoxb-your-bot-token
# Signing Secret
# Slack App > Basic Information > Signing Secret
SLACK_SIGNING_SECRET=your-signing-secret
# App-Level Token (xapp-로 시작, Socket Mode용)
# Slack App > Basic Information > App-Level Tokens
SLACK_APP_TOKEN=xapp-your-app-token
# 식단표를 발송할 Slack 채널 ID (C로 시작)
# 채널 우클릭 > 채널 세부정보 보기 > 하단의 채널 ID
SLACK_CHANNEL_ID=C0123456789
# ─────────────────────────────────────────
# Kakao Channel (선택)
# ─────────────────────────────────────────
# 크롤링 대상 카카오 채널 URL (기본값 사용 가능)
KAKAO_CHANNEL_URL=https://pf.kakao.com/_LCxlxlxb/posts
# ─────────────────────────────────────────
# Server
# ─────────────────────────────────────────
PORT=3000
NODE_ENV=development # production | development- Slack API 접속
- Create New App > From scratch
- App Name:
냠냠위듀(또는 원하는 이름) - Workspace 선택 후 생성
OAuth & Permissions 메뉴에서 Bot Token Scopes 추가:
| Scope | 용도 |
|---|---|
chat:write |
메시지 발송 |
commands |
슬래시 커맨드 |
- Socket Mode 메뉴 > Enable Socket Mode ON
- App-Level Token 생성 (이름:
socket-mode) - Scope:
connections:write선택 - 생성된
xapp-토큰을SLACK_APP_TOKEN에 설정
Slash Commands 메뉴에서:
| 항목 | 값 |
|---|---|
| Command | /식단 |
| Short Description | 이번 주 식단표 조회 |
| Usage Hint | (비워둠) |
- Install App 메뉴 > Install to Workspace
- 권한 승인
- 생성된
xoxb-토큰을SLACK_BOT_TOKEN에 설정
식단표를 받을 채널에서:
/invite @냠냠위듀
npm run dev# 빌드 및 실행
docker-compose up -d
# 로그 확인
docker-compose logs -f menu-bot
# 중지
docker-compose down# 헬스체크
curl http://localhost:3001/health
# 응답 예시
{"status":"ok","timestamp":"2026-01-24T12:00:00.000Z","uptime":100}서버 상태 확인
Response
{
"status": "ok",
"timestamp": "2026-01-24T12:00:00.000Z",
"uptime": 3600.123
}최신 식단표 조회 (없으면 크롤링 후 반환)
Success Response
{
"success": true,
"data": {
"post": {
"postId": "123456789",
"title": "이노밸리 구내식당 주간메뉴[01/20-01/24]",
"imageUrl": "https://k.kakaocdn.net/dn/.../menu.jpg",
"publishedAt": "2026-01-20T00:00:00.000Z"
},
"source": "cache" // "cache" | "crawled"
}
}Error Response
{
"success": false,
"error": "크롤링 실패: 필수 데이터 누락"
}이미지를 직접 저장하지 않습니다. 카카오 채널의 CDN URL만 데이터베이스에 저장합니다.
[카카오 채널] [서버 DB] [Slack]
│ │ │
│ 크롤링 │ │
├─────────────────────────────▶│ │
│ 이미지 URL 추출 │ │
│ (https://k.kakaocdn.net/...)│ │
│ │ │
│ │ URL 전달 │
│ ├──────────────────────────▶│
│ │ │
│◀─────────────────────────────┼───────────────────────────│
│ Slack이 URL로 이미지 로드 │ │
- 장점: 저장 공간 불필요, 항상 원본 품질 유지
- 주의: 카카오가 원본 이미지를 삭제하면 표시 불가
node-cron을 사용하여 매주 월요일 오전 9시(KST)에 자동 실행:
┌───────────── 분 (0)
│ ┌─────────── 시 (9)
│ │ ┌───────── 일 (*)
│ │ │ ┌─────── 월 (*)
│ │ │ │ ┌───── 요일 (1 = 월요일)
│ │ │ │ │
0 9 * * 1 Asia/Seoul
실행 흐름:
- 카카오 채널에서 최신 식단표 크롤링
- DB에서 기존 발송 이력 확인
- 새 식단표이고 미발송 상태면 Slack 발송
- 발송 이력 저장 (중복 방지)
- 실패 시 1시간 후 재시도 (최대 6회)
┌─────────────────────┐ ┌─────────────────────┐
│ MenuPost │ │ DeliveryHistory │
├─────────────────────┤ ├─────────────────────┤
│ id (PK) │ │ id (PK) │
│ postId (UNIQUE) │◀──────│ postId (FK) │
│ title │ │ channel │
│ imageUrl │ │ sentAt │
│ publishedAt │ └─────────────────────┘
│ crawledAt │
└─────────────────────┘ UNIQUE(postId, channel)
src/
├── domain/ # 순수 비즈니스 로직 (외부 의존성 없음)
│ ├── entities/ # MenuPost, DeliveryHistory
│ ├── repositories/ # Repository 인터페이스
│ ├── services/ # Service 인터페이스
│ └── value-objects/ # PostId, ImageUrl (불변, 검증됨)
│
├── application/ # UseCase 레이어
│ └── use-cases/
│ ├── CrawlWeeklyMenuUseCase.ts
│ ├── SendMenuToSlackUseCase.ts
│ ├── CheckAndSendMenuUseCase.ts
│ └── GetCurrentMenuUseCase.ts
│
├── infrastructure/ # 외부 시스템 구현체
│ ├── crawling/ # PlaywrightCrawlerService
│ ├── slack/ # SlackBotService, SlackMessageBuilder
│ ├── persistence/ # Prisma Repository 구현체
│ └── scheduling/ # CronScheduler
│
├── interface/ # 진입점
│ ├── http/ # Express 라우터
│ └── slack/ # 슬래시 커맨드 핸들러
│
├── config/ # 설정
│ └── container.ts # TSyringe DI 컨테이너
│
├── shared/ # 공통 모듈
│ ├── types/ # Result<T, E>
│ └── errors/ # DomainError, CrawlingError 등
│
├── app.ts # Express 앱 설정
└── server.ts # 서버 엔트리포인트
# 개발 서버 (hot reload)
npm run dev
# TypeScript 빌드
npm run build
# 테스트 (watch 모드)
npm test
# 테스트 (1회 실행)
npm run test:run
# 테스트 커버리지
npm run test:coverage
# 단일 테스트 파일 실행
npm test -- test/unit/domain/MenuPost.spec.ts
# ESLint
npm run lint
# Prisma Studio (DB GUI)
npm run prisma:studiotest/
├── unit/ # 단위 테스트 (Mock 사용)
│ ├── domain/ # 엔티티, 값 객체
│ └── use-cases/ # UseCase
├── integration/ # 통합 테스트 (실제 DB)
│ └── repositories/ # Repository
├── e2e/ # E2E 테스트
└── fixtures/
└── builders/ # 테스트 데이터 빌더
- Path Aliases:
@domain/*,@application/*등 사용 필수 - Error Handling:
Result<T, E>패턴 사용 (예외 던지지 않음) - DI: TSyringe 데코레이터로 의존성 주입
- 테스트: Vitest + Builder 패턴
자세한 내용은 AGENTS.md 참고
ISC