Foodbox downloads Eisodosirak menu images, parses them with Naver Clova OCR, stores the results in a file-based database, and exposes the lunch menu through a web calendar and Slack notifications.
- Backend: Spring Boot 3.5.14, Java 25, Gradle
- Frontend: Svelte 5, Vite
- OCR: Naver Clova OCR
- Crawling: JSoup, image download
- Storage: file-based JSON database
- Deployment: Docker Compose, Nginx
- Java 25
- Node.js 20+
- npm
- Docker / Docker Compose, if running the production-like stack
Create a .env file from .env.example before running the app locally.
cp .env.example .envRequired values:
SLACK_TOKEN=your_slack_bot_token_here
SLACK_CHANNEL=#your_slack_channel_here
CRAWL_URL=https://eisodosirak.itpage.kr/bbs/board.php?bo_table=basic4
CLOVA_URL=your_clova_api_url_here
CLOVA_SECRET_KEY=your_clova_secret_key_hereDB_FILE_DIR is optional. The development profile uses /tmp/foodbox/db by default.
Docker Compose reads .env automatically. When running locally with ./gradlew bootRun, export the environment variables in the same terminal first.
set -a
source .env
set +aFor development, run the backend with the dev profile because the frontend Vite proxy forwards API requests to localhost:8080.
set -a
source .env
set +a
SPRING_PROFILES_ACTIVE=dev ./gradlew bootRunCheck the backend:
curl http://localhost:8080/api/menuRun the frontend in a separate terminal.
cd front
npm install
npm run devOpen http://localhost:5173 in your browser. Frontend /api/* requests are proxied to http://localhost:8080 by front/vite.config.js.
./gradlew clean build
./gradlew test
SPRING_PROFILES_ACTIVE=dev ./gradlew bootRuncd front
npm install
npm run dev
npm run build
npm run previewBuild the backend JAR before starting Docker.
./gradlew clean build
docker compose up -d
docker compose logs -f foodbox-backendCompose services:
foodbox-backend: Spring Boot app, container port 80foodbox-frontend: Nginx serving Svelte build, host ports 80/443./db: mounted to/dbin the backend container/api/*: proxied by Nginx to the backend service
front/nginx.conf currently assumes the foodbox.o-r.kr domain and Let's Encrypt certificate paths. To test HTTPS locally with Docker, adjust the Nginx config or certificate mounts for your environment.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/menu/today |
Get today's menu |
GET |
/api/menu |
Get all stored menus |
GET |
/api/crawl |
Manually crawl and OCR-parse the menu image |
POST |
/api/upload |
Upload a menu image and OCR-parse it |
GET |
/api/slack/notify |
Manually send today's Slack notification |
/api/crawl and /api/slack/notify change server-side state, but the current implementation uses GET.
.
├── src/main/java/shanepark/foodbox
│ ├── api
│ │ ├── controller # REST API
│ │ ├── domain # Menu, ApiResponse, DTOs
│ │ ├── repository # file-based menu storage
│ │ └── service # menu crawl, parse, lookup flow
│ ├── crawl # vendor page/image crawling
│ ├── image
│ │ ├── domain # parsed menu and OCR regions
│ │ └── ocr # Clova OCR client/parser and margin calculator
│ └── slack # Slack schedule, message formatting, sender
├── src/main/resources
│ ├── application.yml
│ └── application-dev.yml
└── front
├── src # Svelte app
├── vite.config.js # dev API proxy to localhost:8080
└── nginx.conf # production frontend/API proxy
MenuServicestarts up and checks whether stored menu data is up to date.- If data is missing or stale,
MenuCrawlerdownloads the vendor menu image. - The image hash is compared with the previous crawl to avoid duplicate OCR work.
ImageParserClovaEisosends the image to Naver Clova OCR and parses date/menu regions.- Parsed menus are saved by
MenuRepositoryin the configured DB directory. - The Svelte app reads
/api/menuand renders a monthly calendar. SlackNotifyServicesends the daily 9 AM notification, with special Wednesday handling.
- A menu is valid only when it has at least 3 menu items.
- Invalid menus are skipped for Slack notifications and are treated like holiday/no-menu cases.
- Weekends do not send lunch notifications.
- Wednesdays are special:
- first three Wednesdays of a month: Dennis Deli salad day
- last Wednesday of a month: eating-out day
- OCR dates that contain only month and day are resolved to the closest date within the current, previous, or next year window.
Run all tests:
./gradlew testFocused test examples:
./gradlew test --tests MenuRepositoryTest
./gradlew test --tests ImageMarginCalculatorEisoTest
./gradlew test --tests ImageParserClovaEisoTest
./gradlew test --tests SlackNotifyServiceTest
./gradlew test --tests SlackMessageSenderTestOCR parser tests use sample image/OCR resources under src/test/resources.
- Add or update sample menu images and OCR JSON under
src/test/resources. - Adjust region detection in
ImageMarginCalculatorEiso. - Adjust parsing in
ImageParserClovaEisoorParsedMenuEiso. - Run
./gradlew test --tests ImageParserClovaEisoTest. - Run
./gradlew clean build.
- Update
SlackNotifyServiceorNotifyDate. - Add or update cases in
SlackNotifyServiceTest. - Run
./gradlew test --tests SlackNotifyServiceTest.
- Create a vendor-specific
ImageParserClova{Vendor}. - Create a vendor-specific
ImageMarginCalculator{Vendor}. - Add image/OCR fixtures under
src/test/resources. - Update
CRAWL_URL. - Update
MenuCrawler.getMenuImage()if the vendor page structure differs. - Add parser tests before deploying.
- Frontend shows no data: confirm the backend is running on
localhost:8080andcurl http://localhost:8080/api/menureturns JSON. - Frontend API calls fail in dev: check
front/vite.config.js; the proxy target should match the backend port. - Backend starts on port 80: run with
SPRING_PROFILES_ACTIVE=devfor port 8080. - OCR parsing fails: verify
CLOVA_URLandCLOVA_SECRET_KEY, then inspect parser tests and OCR fixtures. - Slack notification fails: verify Slack webhook token/channel config and run
GET /api/slack/notifymanually. - Docker frontend fails on HTTPS locally:
front/nginx.confexpects production certificate paths forfoodbox.o-r.kr.
