Skip to content

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.

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

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.

  • Cafeteria numbers are the school’s values. Numbers can change, and place_name in 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 with cafeteria_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).
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();
}
}