기숙사·학식 봇
오늘의 기숙사 식단, 학식, 기숙사 공지를 메시지 문자열로 만듭니다. 슬랙, 디스코드, 카카오 등 보내는 쪽에 붙이면 됩니다. 로그인이 필요 없습니다.
요청 파라미터
섹션 제목: “요청 파라미터”필드 이름은 Python snake_case 기준이고, Node.js는 camelCase 객체 인자({ rows: 3 }), Java는 위치 인자입니다. 로그인은 필요 없습니다.
| 메서드 | 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|---|
dormitory_menu |
location |
문자열 | 아니오 | SJ |
기숙사 위치 코드 |
cafeteria_menu |
place_id |
문자열 | 예 | 학교의 식당 번호 (호출하는 쪽이 넘김) | |
cafeteria_places |
max_place_id |
정수 | 아니오 | 10 |
식당 번호 1~이 값을 훑어 목록을 만듦. 식당이 늘면 올림 |
dormitory_notices |
rows |
정수 | 아니오 | 10 |
한 번에 받을 공지 수 |
page |
정수 | 아니오 | 1 |
공지 쪽 번호 | |
keyword |
문자열 | 아니오 | 빈 문자열 | 제목 검색어 | |
location |
문자열 | 아니오 | SJ |
기숙사 위치 코드 |
응답 데이터 구조
섹션 제목: “응답 데이터 구조”예시 코드가 메시지로 만드는 원본 데이터입니다. 필드 이름은 Python snake_case이고, Node.js는 camelCase(meal_name → mealName), Java는 getter(getMealName())입니다. 값은 형태를 보여 주기 위한 예시입니다.
dormitory_menu(): 하루 한 건, 최대 7일치 목록
[ { "date": "2026-03-02", "breakfast": "쌀밥 된장국 계란말이", "lunch": "쌀밥 육개장 부추전 배추김치", "dinner": "쌀밥 북어국 안동찜닭 배추김치", "alternative": "식권 1장으로 간편식 교환" }]| 필드 | 의미 |
|---|---|
date |
날짜 (yyyy-MM-dd) |
breakfast / lunch / dinner |
조식 / 중식 / 석식. 학교 원문이라 반찬이 한 문자열에 이어져 있음 |
alternative |
대체 메뉴 (없으면 빈 문자열) |
cafeteria_menu(place_id): 메뉴 한 줄이 한 건. 식당에 따라 날짜가 있는 주간 메뉴이거나 날짜가 빈 상시 메뉴
[ { "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" }]| 필드 | 의미 |
|---|---|
place_id / place_name |
식당 번호 / 이름 |
building_name / location |
건물 / 층 |
date / day |
날짜 / 요일. 상시 메뉴 식당은 빈 문자열 |
meal_name |
식사 구분 (중식, 석식 등). 상시 메뉴는 빈 문자열 |
menu_name |
메뉴 (여러 개는 ·로 이어짐) |
price |
가격 (원, 문자열) |
dormitory_notices(rows=3): 공지 목록. 고정 공지(pinned)와 일반 공지(items)가 나뉘어 있음
{ "total": 120, "pinned": [], "items": [ { "id": "37570", "title": "[기숙사] 정기소독 실시 안내", "date": "2026-03-02", "views": 426, "file_name": "안내.png" } ]}| 필드 | 의미 |
|---|---|
total |
전체 공지 수 (정수) |
pinned / items |
고정 공지 / 일반 공지 목록. 항목 모양은 같음 |
id / title / date |
공지 번호 / 제목 / 등록일 |
views / file_name |
조회수 (정수) / 첨부 파일 이름 (없으면 빈 문자열) |
실패 시: 학교 서버가 실패하면 예외 없이 마지막 캐시 또는 빈 목록(dormitory_notices는 total이 0인 빈 결과)을 돌려줍니다. 빈 목록은 「데이터 없음」과 구분되지 않으니 화면에 안내를 두세요.
알아둘 점
섹션 제목: “알아둘 점”- 식당 번호는 학교 값입니다. 번호는 바뀔 수 있고, 어느 식당인지는 결과의
place_name으로 알 수 있습니다. 빈 목록은 번호가 틀렸을 때와 그날 메뉴가 없을 때 모두 나옵니다. 식당 목록은cafeteria_places()로도 받을 수 있습니다. 학식 - 식당에 따라 일주일치가 날짜와 함께 오거나, 날짜 없는 상시 메뉴가 옵니다. 예시는 둘 다 처리합니다.
- 기숙사 식단은 7일치이며
dormitory_menu(location="SJ")입니다. 기숙사 - 학교 서버에 부담을 주지 않도록 봇은 하루 몇 번만 갱신해도 충분합니다 (라이브러리 캐시가 이미 24시간).
예시 코드
섹션 제목: “예시 코드”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"] # 학교의 식당 번호, 바뀔 수 있음
def dormitory_menu_message() -> str: today = date.today().isoformat() menu_days = public_data.dormitory_menu() # 오늘 날짜가 없으면(주말 갱신 전 등) 학교가 준 첫날을 보여 줌 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: # 식당 번호가 없거나 그날 메뉴가 없으면 빈 목록 (둘을 구분하지 못함) return f"{place_id}번 식당: 메뉴 없음" today = date.today().isoformat() # 날짜가 있는 식당은 일주일치가 한 번에 오므로 오늘 것만, 날짜가 없는 식당(상시 메뉴)은 그대로 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']; // 학교의 식당 번호, 바뀔 수 있음const todayText = () => new Date().toLocaleDateString('sv-SE'); // yyyy-MM-dd (로컬 날짜)
export async function dormitoryMenuMessage() { const menuDays = await publicData.dormitoryMenu(); // 오늘 날짜가 없으면(주말 갱신 전 등) 학교가 준 첫날을 보여 줌 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}번 식당: 메뉴 없음`; // 식당 번호가 없거나 그날 메뉴가 없으면 빈 목록 (둘을 구분하지 못함) // 날짜가 있는 식당은 일주일치가 한 번에 오므로 오늘 것만, 날짜가 없는 식당(상시 메뉴)은 그대로 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());