Dormitory and cafeteria bot
Builds today’s dormitory menu, cafeteria menus and dormitory notices as message strings. Attach it to whatever sends them: Slack, Discord, Kakao and so on. No login is needed.
The message labels in the code (조식, 중식, 석식, 번 식당) are Korean, matching the school’s own data; replace them with your own wording.
Request parameters
Section titled “Request parameters”Names follow Python snake_case; Node.js takes a camelCase options object ({ rows: 3 }) and Java takes positional arguments. No login is needed.
| Method | Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|---|
dormitory_menu |
location |
string | No | SJ |
Dormitory location code |
cafeteria_menu |
place_id |
string | Yes | The school’s cafeteria number (passed by the caller) | |
cafeteria_places |
max_place_id |
integer | No | 10 |
Scans numbers 1 to this value to build the list. Raise it when cafeterias are added |
dormitory_notices |
rows |
integer | No | 10 |
Notices per request |
page |
integer | No | 1 |
Page number | |
keyword |
string | No | empty string | Title search text | |
location |
string | No | SJ |
Dormitory location code |
Response data structure
Section titled “Response data structure”The raw data the example turns into messages. Field names are Python snake_case; Node.js uses camelCase (meal_name -> mealName) and Java uses getters (getMealName()). Values only show the shape.
dormitory_menu(): one entry per day, up to 7 days
[ { "date": "2026-03-02", "breakfast": "쌀밥 된장국 계란말이", "lunch": "쌀밥 육개장 부추전 배추김치", "dinner": "쌀밥 북어국 안동찜닭 배추김치", "alternative": "식권 1장으로 간편식 교환" }]| Field | Meaning |
|---|---|
date |
Date (yyyy-MM-dd) |
breakfast / lunch / dinner |
Meals. The school’s own text, so several dishes sit in one string |
alternative |
Alternative meal (empty string if none) |
cafeteria_menu(place_id): one menu line per entry. Depending on the cafeteria, a dated weekly menu or an undated standing menu
[ { "place_id": "1", "place_name": "계절밥상", "building_name": "군자관", "location": "6층", "date": "2026-03-02", "day": "월", "meal_type_id": "1", "meal_name": "중식", "menu_name": "된장국 · 제육볶음 · 배추김치", "price": "7000" }, { "place_id": "3", "place_name": "진관키친", "building_name": "진관홀", "location": "B1층", "date": "", "day": "", "meal_type_id": "", "meal_name": "", "menu_name": "라면", "price": "3300" }]| Field | Meaning |
|---|---|
place_id / place_name |
Cafeteria number / name |
building_name / location |
Building / floor |
date / day |
Date / weekday. Empty string for standing-menu cafeterias |
meal_name |
Meal (lunch, dinner, …). Empty string for standing menus |
menu_name |
Menu (several dishes joined with ·) |
price |
Price (KRW, as a string) |
dormitory_notices(rows=3): notices, split into pinned (pinned) and regular (items)
{ "total": 120, "pinned": [], "items": [ { "id": "37570", "title": "[기숙사] 정기소독 실시 안내", "date": "2026-03-02", "views": 426, "file_name": "안내.png" } ]}| Field | Meaning |
|---|---|
total |
Total number of notices (integer) |
pinned / items |
Pinned / regular notice lists. Entries have the same shape |
id / title / date |
Notice number / title / posted date |
views / file_name |
View count (integer) / attachment file name (empty string if none) |
All fields: Dormitory and Cafeterias.
On failure: when the school server fails you get the last cached value or an empty list (an empty result with total 0 for dormitory_notices), without an exception. An empty list looks the same as “no data”, so show a hint.
Good to know
Section titled “Good to know”- Cafeteria numbers are the school’s values. Numbers can change, and
place_namein the result tells which cafeteria it is. An empty list appears both for a wrong number and for a day without a menu. You can also list cafeterias withcafeteria_places(). Cafeterias - Depending on the cafeteria, you get a week of dated menus or an undated standing menu. The examples handle both.
- The dormitory menu covers 7 days:
dormitory_menu(location="SJ"). Dormitory - To go easy on the school servers, refreshing a few times a day is enough (the library cache is already 24 hours).
Example code
Section titled “Example code”import java.time.LocalDate;import java.util.List;import kr.suhsaechan.sejongpeia.publicdata.SejongPublicData;import kr.suhsaechan.sejongpeia.publicdata.model.CafeteriaMenuItem;import kr.suhsaechan.sejongpeia.publicdata.model.DormitoryMenuDay;
class CampusInfoBot {
static String dormitoryMenuMessage(SejongPublicData publicData) { String today = LocalDate.now().toString(); List<DormitoryMenuDay> menuDays = publicData.dormitoryMenu(); DormitoryMenuDay menuDay = menuDays.stream().filter(day -> today.equals(day.getDate())).findFirst() .orElse(menuDays.isEmpty() ? null : menuDays.get(0)); if (menuDay == null) { return "기숙사 식단을 불러오지 못했습니다"; } return "[기숙사 " + menuDay.getDate() + "]\n조식: " + menuDay.getBreakfast() + "\n중식: " + menuDay.getLunch() + "\n석식: " + menuDay.getDinner(); }
static String cafeteriaMessage(SejongPublicData publicData, String placeId) { String today = LocalDate.now().toString(); List<CafeteriaMenuItem> items = publicData.cafeteriaMenu(placeId); if (items.isEmpty()) { return placeId + "번 식당: 메뉴 없음"; } List<CafeteriaMenuItem> todaysItems = items.stream() .filter(item -> item.getDate().isEmpty() || item.getDate().equals(today)).toList(); List<CafeteriaMenuItem> shownItems = todaysItems.isEmpty() ? items : todaysItems; StringBuilder message = new StringBuilder("[" + shownItems.get(0).getPlaceName() + "]"); shownItems.stream().limit(5).forEach(item -> message.append("\n").append(item.getMealName()).append(" ").append(item.getMenuName()).append(" ").append(item.getPrice()).append("원")); return message.toString(); }}from datetime import date
from sejongpeia import SejongPublicData
public_data = SejongPublicData()CAFETERIA_PLACE_IDS = ["1", "3", "4", "5", "6"] # the school's cafeteria numbers, may change
def dormitory_menu_message() -> str: today = date.today().isoformat() menu_days = public_data.dormitory_menu() # If today is missing (before a weekend refresh, for example) show the first day the school gave menu_day = next((day for day in menu_days if day.date == today), menu_days[0] if menu_days else None) if menu_day is None: return "기숙사 식단을 불러오지 못했습니다" return f"[기숙사 {menu_day.date}]\n조식: {menu_day.breakfast}\n중식: {menu_day.lunch}\n석식: {menu_day.dinner}"
def cafeteria_message(place_id: str) -> str: items = public_data.cafeteria_menu(place_id) if not items: # empty for a wrong number and for a day without a menu (cannot tell them apart) return f"{place_id}번 식당: 메뉴 없음" today = date.today().isoformat() # Dated cafeterias return a week at once, so keep today only; undated ones (standing menus) stay as they are todays_items = [item for item in items if item.date in ("", today)] or items first = todays_items[0] lines = [f"{item.meal_name} {item.menu_name} {item.price}원".strip() for item in todays_items[:5]] return f"[{first.place_name} {first.building_name} {first.location}]\n" + "\n".join(lines)
def latest_dormitory_notice_message() -> str: notices = public_data.dormitory_notices(rows=3) return "\n".join(f"{notice.date} {notice.title}" for notice in notices.items) or "공지를 불러오지 못했습니다"
if __name__ == "__main__": print(dormitory_menu_message()) for place_id in CAFETERIA_PLACE_IDS: print(cafeteria_message(place_id)) print(latest_dormitory_notice_message())import { SejongPublicData } from 'pro-sejongpeia';
const publicData = new SejongPublicData();const CAFETERIA_PLACE_IDS = ['1', '3', '4', '5', '6']; // the school's cafeteria numbers, may changeconst todayText = () => new Date().toLocaleDateString('sv-SE'); // yyyy-MM-dd (local date)
export async function dormitoryMenuMessage() { const menuDays = await publicData.dormitoryMenu(); // If today is missing (before a weekend refresh, for example) show the first day the school gave const menuDay = menuDays.find((day) => day.date === todayText()) ?? menuDays[0]; if (!menuDay) return '기숙사 식단을 불러오지 못했습니다'; return `[기숙사 ${menuDay.date}]\n조식: ${menuDay.breakfast}\n중식: ${menuDay.lunch}\n석식: ${menuDay.dinner}`;}
export async function cafeteriaMessage(placeId) { const items = await publicData.cafeteriaMenu(placeId); if (items.length === 0) return `${placeId}번 식당: 메뉴 없음`; // empty for a wrong number and for a day without a menu (cannot tell them apart) // Dated cafeterias return a week at once, so keep today only; undated ones (standing menus) stay as they are const todaysItems = items.filter((item) => item.date === '' || item.date === todayText()); const shownItems = todaysItems.length > 0 ? todaysItems : items; const lines = shownItems.slice(0, 5).map((item) => `${item.mealName} ${item.menuName} ${item.price}원`.trim()); const first = shownItems[0]; return `[${first.placeName} ${first.buildingName} ${first.location}]\n${lines.join('\n')}`;}
export async function latestDormitoryNoticeMessage() { const notices = await publicData.dormitoryNotices({ rows: 3 }); return notices.items.map((notice) => `${notice.date} ${notice.title}`).join('\n') || '공지를 불러오지 못했습니다';}
console.log(await dormitoryMenuMessage());for (const placeId of CAFETERIA_PLACE_IDS) console.log(await cafeteriaMessage(placeId));console.log(await latestDormitoryNoticeMessage());