# PRO-SEJONGPEIA > Everything you need to build on Sejong University. Student login, departments, lectures, calendar and cafeterias for Java, Python and Node.js. Source: https://cassiiopeia.github.io/pro-sejongpeia/en/ --- # Introduction Source: https://cassiiopeia.github.io/pro-sejongpeia/en/start/introduction/ Section: Start pro-sejongpeia is an **integrated library for Sejong University**. It offers the **portal login** that verifies a student with a student ID and password, and the **school data** the university serves publicly without login (department codes, course listings, the academic calendar, dormitory menus, cafeterias and more). Java, Python and Node.js get the same features, the same result fields and the same error codes. The portal login, SSO redirects, session cookies, the old TLS setup and HTML and JSON parsing are handled by the library. > **At a glance** > - **Student verification** needs a student ID and password; **school data** does not. > - Java, Python and Node.js share the same features, result fields and error codes. > - It follows the unofficial addresses the school's web pages use, so it can stop working when the school changes them. ## How it works at a glance One call goes through the library to the school servers, and the response comes back in the same shape. > Diagram: Your app makes one call, the library handles caching, TLS and User-Agent, and failure isolation while talking to the school servers, and returns a result with the same shape ## Who it is for - **Sejong student verification** for clubs, student councils and campus communities - Checking participation in campus events that depend on department or year - Department and lecture search pages, academic calendar widgets - Bots that post the dormitory menu and cafeteria menus ## What you can use Get name, major, year, enrollment status, classic-reading certification and contact details. One call is one portal login. [Authentication reference](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/methods/) Department and term codes, course listings, curriculum, the academic calendar (undergraduate and graduate), dormitory, cafeterias, campus places, extracurricular programs. [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/) Result fields and error codes are the same in all three languages; only the naming follows each language's convention. Shared values live in one place, `spec/contract.json`. With public data, one source failing does not affect the others. A failure returns the last cached value or an empty list. [Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/) ## Data sources | Source | Domain | What it provides | Login | |---|---|---|---| | DHC (Daeyang Humanity College) | classic.sejong.ac.kr | Student info + classic-reading certification | required | | SIS (student information system) | sjpt.sejong.ac.kr | Student info + email, phone, English name, school codes | required | | SIS public lookups | sjpt.sejong.ac.kr | Organization categories, terms, departments, course listings | not needed | | University website | www.sejong.ac.kr | Academic calendar, cafeterias, campus places, curriculum | not needed | | Graduate school sites | `.sejong.ac.kr` | Graduate academic calendar | not needed | | Dormitory | happydorm.sejong.ac.kr | Dormitory menu, notices | not needed | | Do-Dream | do.sejong.ac.kr | Extracurricular program schedule | not needed | ## Good to know - **This is not an official Sejong University product.** It is a personal open-source project by a Sejong student and is not affiliated with the university. - It follows **unofficial addresses used by the university's own web screens**, not a documented API. If the university changes them it can stop working; a daily check opens an issue when something breaks. There is no promise about how fast it gets fixed. - **Backward compatibility is not guaranteed.** Breaking changes can happen even within `1.x`, so pin the exact version. [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/) - School values are **the strings the school sent, as they are**. The library carries no code tables or name mappings. ## Requirements | Java / Spring Boot | Python | Node.js | |---|---|---| | Java 17+, Spring Boot 3.x. Plain Java works too | Python 3.9+, no dependencies beyond the standard library | Node 18+, no dependencies, TypeScript types included, ESM and CommonJS. Server only | Java uses OkHttp for HTTP and jsoup for HTML parsing. ## Which page should I read? Pick what you need and it leads to the right guide. Click a box to open that page. > Diagram: Pick the page to read by what you need: student verification, lecture search, academic calendar, dormitory and cafeteria, or other school data ## Next - [Quick start](https://cassiiopeia.github.io/pro-sejongpeia/en/start/quickstart/): Install and make your first call in a few minutes. - [Add student verification](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/student-verification/): Verify enrollment with the student ID and password from a sign-up form. - [School data reference](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/): Methods, requests, result fields, cache and failure behavior. - [Try it in the browser](https://cassiiopeia.github.io/pro-sejongpeia/en/#demo): See the real result with your own portal account. --- # Quick start Source: https://cassiiopeia.github.io/pro-sejongpeia/en/start/quickstart/ Section: Start > **At a glance** > Three steps: install, call student verification (from a server only), and call login-free school data. Plain Java without Spring is at the bottom. 1. **Install it.** Pin the exact version. All versions are on [GitHub Releases](https://github.com/Cassiiopeia/pro-sejongpeia/releases) ([API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/)). **Java** [![Maven Central](https://raw.githubusercontent.com/Cassiiopeia/pro-sejongpeia/main/docs/assets/badges/maven-central.svg)](https://central.sonatype.com/artifact/kr.suhsaechan/sejongpeia) Published to Maven Central, so no extra repository is needed. Java 17+, Spring Boot 3.x. ```groovy dependencies { implementation "kr.suhsaechan:sejongpeia:" } ``` ```xml kr.suhsaechan sejongpeia ``` Put the latest version in ``. Check it on the [Maven Central artifact page](https://central.sonatype.com/artifact/kr.suhsaechan/sejongpeia); all versions are listed in the [repository index](https://repo1.maven.org/maven2/kr/suhsaechan/sejongpeia/). **Python** Python 3.9+, no dependencies beyond the standard library. ```bash pip install pro-sejongpeia== ``` **Node.js** Node 18+, zero dependencies, TypeScript types included, ESM and CommonJS. ```bash npm install pro-sejongpeia@ ``` 2. **Call student verification.** One integrated call with a student ID and password. **Java** Spring Boot auto-configuration registers a `SuhSejongAuthEngine` bean. ```java @RestController @RequiredArgsConstructor public class AuthController { private final SuhSejongAuthEngine sejongAuthEngine; @PostMapping("/auth/sejong") public SejongAuthResult authenticate(@RequestParam String studentId, @RequestParam String password) { return sejongAuthEngine.authenticate(studentId, password); } } ``` **Python** ```python from sejongpeia import authenticate, SejongAuthError, ErrorCode try: result = authenticate("21011234", "password") print(result.name, result.major, result.grade) except SejongAuthError as error: if error.code == ErrorCode.AUTHENTICATION_FAILED: print("Wrong student ID or password") elif error.code == ErrorCode.ACCOUNT_LOCKED: print("The portal account is locked. Reset the password on the portal") ``` **Node.js** ```js import { authenticate, SejongAuthError, ErrorCode } from 'pro-sejongpeia'; try { const result = await authenticate('21011234', 'password'); console.log(result.name, result.major, result.grade); } catch (error) { if (error instanceof SejongAuthError && error.code === ErrorCode.AUTHENTICATION_FAILED) { console.log('Wrong student ID or password'); } else if (error instanceof SejongAuthError && error.code === ErrorCode.ACCOUNT_LOCKED) { console.log('The portal account is locked. Reset the password on the portal'); } } ``` Branch on the error **code**, never on the message. The code list is in [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/). 3. **Call school data that needs no login.** Get the department list, then the course listings of the first department (undergraduate). `"20"` (undergraduate) and the semester value are the school's own values. **Java** ```java SejongPublicData publicData = new SejongPublicData(); // inject it as a bean in Spring List departments = publicData.departments("20", "2026", "20"); List lectures = publicData.lectures("20", "2026", "20", departments.get(0).getCode(), "", ""); ``` **Python** ```python from sejongpeia import SejongPublicData public_data = SejongPublicData() departments = public_data.departments("20", "2026", "20") # undergraduate lectures = public_data.lectures("20", "2026", "20", department_code=departments[0].code) # Course number of a lecture > grade, semester and overview in that department's curriculum (empty list if none) courses = public_data.curriculum_courses(lectures[0].department_code, lectures[0].course_number) ``` **Node.js** ```js import { SejongPublicData } from 'pro-sejongpeia'; const publicData = new SejongPublicData(); const departments = await publicData.departments({ categoryCode: '20', year: '2026', semesterCode: '20' }); const lectures = await publicData.lectures({ categoryCode: '20', year: '2026', semesterCode: '20', departmentCode: departments[0].code }); ``` Create **one** `SejongPublicData` per process. The in-memory cache (24 hours by default) and request coalescing live inside the instance. ## Plain Java (without Spring) Spring Boot is an optional dependency, so a plain Java project does not pull it in. The JVM blocks the old TLS cipher the portal accepts, so call `SejongTlsSupport.ensureLegacyCipherEnabled()` **first in `main()`**, before any other HTTPS call. ```java import kr.suhsaechan.sejongpeia.auth.client.SejongPortalClient; import kr.suhsaechan.sejongpeia.auth.client.SejongSisClient; import kr.suhsaechan.sejongpeia.auth.model.SejongAuthResult; import kr.suhsaechan.sejongpeia.auth.parser.SejongClassicReadingParser; import kr.suhsaechan.sejongpeia.auth.parser.SejongSisParser; import kr.suhsaechan.sejongpeia.auth.parser.SejongStudentInfoParser; import kr.suhsaechan.sejongpeia.auth.service.SuhSejongAuthEngine; import kr.suhsaechan.sejongpeia.common.config.SejongAuthProperties; import kr.suhsaechan.sejongpeia.common.util.SejongTlsSupport; SejongTlsSupport.ensureLegacyCipherEnabled(); // first in main(), before any other HTTPS call SejongAuthProperties properties = new SejongAuthProperties(); SuhSejongAuthEngine engine = new SuhSejongAuthEngine(new SejongPortalClient(properties), new SejongStudentInfoParser(), new SejongClassicReadingParser(), new SejongSisClient(properties), new SejongSisParser()); SejongAuthResult result = engine.authenticate(studentId, password); ``` Called too late, authentication fails with `CONNECTION_FAILED` and a message that names the cause. The setting applies to the whole JVM. See [Legacy TLS](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/legacy-tls/) for why. Spring Boot handles it at startup, and Python and Node change only the TLS settings of their own connections. > **Call it from a server only** > Calling from a browser or app exposes the password to third-party code. The Node package is server-only too. A wrong password sent repeatedly makes the portal lock the account, so if anyone can type a student ID into your form, limit failures per student ID. [Login verdict and account lock protection](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/) > **What an empty list means** > When a school server fails, public data returns the last cached value or an empty list without throwing. An empty list can mean "no data" or "a problem on the school side", so show a hint such as "could not load" next to empty results. ## Next - [Add student verification](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/student-verification/): A club sign-up form example and what to watch for. - [Department and lecture search](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/department-lecture-search/): A small server that returns departments and lectures as JSON. - [Authentication configuration](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/configuration/): Timeout, retries, certificate verification. --- # Add student verification Source: https://cassiiopeia.github.io/pro-sejongpeia/en/guides/student-verification/ Section: Guides Take the student ID and password from a sign-up form, check enrollment, and fetch only student ID, name and major. The password is neither stored nor logged. > **The example code is for reference** > Method names, arguments, result fields and error codes follow [RESULT_SCHEMA](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/RESULT_SCHEMA.md). With a real portal account, `scripts/verify-live.sh` in the repository compares the three languages ([Issues and pull requests](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/issues-and-prs/)). ## Verification flow A wrong password stops at the first failure. There is no retry, so the library never locks the account. > Diagram: After the portal login the library reads the verdict inside the response. A wrong password or a locked account stops at the first failure with an error code, and success queries DHC and SIS in the same session ## Request parameters Names follow Python `snake_case`; Node.js and Java use `camelCase`. A portal login is required. | Method | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---|---| | `authenticate` | `student_id` | string | Yes | | Student ID | | | `password` | string | Yes | | Portal password. Never stored or logged | | | `include_raw` | boolean | No | `false` | Also return the portal's raw text (`rawHtml`, `rawJson`) | | | `include_extra` | boolean | No | `false` | Return the school's remaining SIS fields as `extra` | | `authenticate_dhc` | `student_id`, `password`, `include_raw` | | | | When only student ID, name, major, year and status are needed (lighter, no contact lookup) | ## Response data structure The result `authenticate` returns (top) and the value the example function hands back to the sign-up form (bottom). Field names are Python `snake_case` (`student_id`); Node.js and Java use `camelCase` (`studentId`, `getStudentId()`). Values are made-up and only show the shape. ```json { "success": true, "student_id": "20000000", "name": "홍길동", "major": "컴퓨터공학과", "grade": "3", "status": "재학", "email": "user@example.com", "phone_number": "010-0000-0000", "english_name": "Gildong Hong", "user_category": "학부생" } ``` ```json { "verified": true, "student_id": "20000000", "name": "홍길동", "major": "컴퓨터공학과" } ``` | Field | Meaning | |---|---| | `student_id` / `name` / `major` | Student ID / name / major. What the example function returns | | `status` | Enrollment status (enrolled, on leave, graduated, ...). The portal's own text, so the wording can change | | `grade` | Year of study (string) | | `email` / `phone_number` / `english_name` / `user_category` | Contact (SIS) lookup. `null` when the SIS lookup fails. The other SIS school-code fields are omitted here | | `verified` | Built by the example function by comparing `status` with the enrolled value. Not a library field | All fields: [Result fields](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/result-fields/). **On failure**: a wrong password, a locked account and so on are thrown as `SejongAuthError` (with an error code). [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/) ## Good to know - **Never resend a wrong password repeatedly.** The portal locks the account. The library stops at the first failure, but if anyone can type a student ID into your form, **limit failures per student ID**. [Login verdict and account lock protection](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/) - `status` and `major` are the portal's own strings. The `재학` comparison above is an example; keep it as a constant in one place since the wording can change. - One `authenticate` call includes the contact (SIS) lookup. If you only need student ID, name and major, the DHC-only call `authenticate_dhc` / `authenticateDhc` / `authenticateWithDHC` is lighter. See [Authentication methods](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/methods/) and [Result fields](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/result-fields/). - Call it from a server only. Calling from a browser or app exposes the password to third-party code. - Branch on the error **code**, never on the message. [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/) ## Example code The user-facing messages are in Korean in the original examples; replace them with your own. `ACTIVE_STATUS = "재학"` is the portal's own string for "enrolled" and must stay as the school sends it. **Java** ```java import kr.suhsaechan.sejongpeia.auth.model.SejongAuthResult; import kr.suhsaechan.sejongpeia.auth.service.SuhSejongAuthEngine; import kr.suhsaechan.sejongpeia.common.exception.SejongAuthErrorCode; import kr.suhsaechan.sejongpeia.common.exception.SejongAuthException; import org.springframework.stereotype.Service; @Service class StudentVerificationService { private static final String ACTIVE_STATUS = "재학"; // compare with the school's own string private final SuhSejongAuthEngine sejongAuthEngine; StudentVerificationService(SuhSejongAuthEngine sejongAuthEngine) { this.sejongAuthEngine = sejongAuthEngine; } /** Return only name and major; the password is not kept */ String verifySejongStudent(String studentId, String password) { try { SejongAuthResult result = sejongAuthEngine.authenticate(studentId, password); boolean isActiveStudent = ACTIVE_STATUS.equals(result.getStatus()); return isActiveStudent ? result.getName() + " (" + result.getMajor() + ")" : "Not an enrolled student"; } catch (SejongAuthException exception) { SejongAuthErrorCode errorCode = exception.getErrorCode(); if (errorCode == SejongAuthErrorCode.AUTHENTICATION_FAILED) { return "Wrong student ID or password"; } if (errorCode == SejongAuthErrorCode.ACCOUNT_LOCKED) { return "The portal account is locked. Reset the password on the portal"; } return "Cannot verify right now. Try again later"; } } } ``` **Python** ```python from sejongpeia import authenticate, SejongAuthError, ErrorCode ACTIVE_STATUS = "재학" # compare with the school's own string (change only here if the portal wording changes) def verify_sejong_student(student_id: str, password: str) -> dict: """Student verification for club sign-up: return only ID, name and major; the password is not kept""" try: result = authenticate(student_id, password) except SejongAuthError as error: if error.code == ErrorCode.AUTHENTICATION_FAILED: return {"verified": False, "reason": "Wrong student ID or password"} if error.code == ErrorCode.ACCOUNT_LOCKED: return {"verified": False, "reason": "The portal account is locked. Reset the password on the portal"} if error.code == ErrorCode.INVALID_INPUT: return {"verified": False, "reason": "Enter your student ID and password"} return {"verified": False, "reason": "Cannot verify right now. Try again later"} # connection, parsing, etc. return { "verified": result.status == ACTIVE_STATUS, "student_id": result.student_id, "name": result.name, "major": result.major, } ``` **Node.js** ```js import { authenticate, SejongAuthError, ErrorCode } from 'pro-sejongpeia'; const ACTIVE_STATUS = '재학'; // compare with the school's own string (change only here if the portal wording changes) /** Student verification for club sign-up: return only ID, name and major; the password is not kept */ export async function verifySejongStudent(studentId, password) { try { const result = await authenticate(studentId, password); return { verified: result.status === ACTIVE_STATUS, studentId: result.studentId, name: result.name, major: result.major }; } catch (error) { if (!(error instanceof SejongAuthError)) throw error; if (error.code === ErrorCode.AUTHENTICATION_FAILED) return { verified: false, reason: 'Wrong student ID or password' }; if (error.code === ErrorCode.ACCOUNT_LOCKED) return { verified: false, reason: 'The portal account is locked. Reset the password on the portal' }; if (error.code === ErrorCode.INVALID_INPUT) return { verified: false, reason: 'Enter your student ID and password' }; return { verified: false, reason: 'Cannot verify right now. Try again later' }; // connection, parsing, etc. } } ``` --- # Department and lecture search Source: https://cassiiopeia.github.io/pro-sejongpeia/en/guides/department-lecture-search/ Section: Guides A small server that returns departments and lectures as JSON. A front end only needs these two URLs. ## Request parameters The example server's HTTP requests (all `GET`), and the library methods it calls. Names follow Python `snake_case`; Node.js takes a `camelCase` options object and Java takes positional arguments. No login is needed. **Example server** | URL | Parameter | Required | Meaning | |---|---|---|---| | `/departments` | | | No parameters | | `/lectures` | `department` | No | Department code | | | `name` | No | Course name (substring). Requiring one of the two is a rule of this example server | **Library methods** | Method | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---|---| | `departments` | `category_code` | string | Yes | | Organization category (`20` undergraduate) | | | `year` | string | Yes | | Year | | | `semester_code` | string | Yes | | Semester code (`10` first, `20` second, `11` and `21` seasonal) | | `lectures` | `category_code`, `year`, `semester_code` | string | Yes | | Same as above | | | `department_code` | string | No | empty string | Department code | | | `course_name` | string | No | empty string | Course name | | | `instructor_name` | string | No | empty string | Instructor name. With all three conditions empty you get the whole term | The example code targets undergraduate (`"20"`); year and semester (`"20"` = second semester) are constants at the top of the code. All code values are the school's own; list them with `organization_categories()` and `terms(...)` ([Department and term codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/)). ## Response data structure The JSON the example server returns. The top block is the response of `GET /departments`, the bottom one of `GET /lectures?...`; values only show the shape. Key names follow the Python example; in the Node.js example `course_number` is `courseNumber`. The Java controller returns the library models (`Department`, `Lecture`) as they are, so it has more fields, named in `camelCase`. ```json [ { "code": "3517", "college": "인공지능융합대학", "name": "AI로봇학과" }, { "code": "3331", "college": "연계전공", "name": "스마트투어리즘매니지먼트소프트웨어" } ] ``` ```json [ { "course": "알고리즘및실습", "department": "컴퓨터공학과", "schedule": "화13:00-15:00", "credits": "3.0", "course_number": "009954" } ] ``` | Field | Meaning | |---|---| | `code` | Department code. Pass it as-is to `/lectures?department=` | | `college` / `name` | College / department (or major) name | | `course` / `department` | Course name / offering department name | | `schedule` | Weekday and time (school text, for example `화13:00-15:00`) | | `credits` | Credits (string) | | `course_number` | Course number. Usable to continue to the curriculum lookup | For every field the library returns, see [Course listings](https://cassiiopeia.github.io/pro-sejongpeia/en/data/lectures/#response-data-structure) and [Department and term codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/#response-data-structure). **On failure**: when the school server fails you get the last cached value or an empty list, without an exception. The example server returns it as an empty array (`[]`). ## Good to know - Department code, course name and instructor name of `lectures` are all optional. **With all of them empty you get the whole term**: a response of several MB ([Course listings](https://cassiiopeia.github.io/pro-sejongpeia/en/data/lectures/)), so the example server requires a condition. If you need everything, query department by department. - Lecture results include the instructor name (`instructor`). The examples return only selected fields; decide which fields to expose when you publish this. - Course-name search matches substrings. Some names return 0 results, so show an empty-result hint. - A lecture's `course_number` and `department_code` lead on to the department curriculum entry (grade, semester, course overview) with `curriculum_courses`. [Curriculum](https://cassiiopeia.github.io/pro-sejongpeia/en/data/curriculum/) ## Example code **Java** ```java import java.util.List; import kr.suhsaechan.sejongpeia.publicdata.SejongPublicData; import kr.suhsaechan.sejongpeia.publicdata.model.Department; import kr.suhsaechan.sejongpeia.publicdata.model.Lecture; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController class LectureSearchController { private static final String UNDERGRADUATE_CATEGORY = "20"; private static final String YEAR = "2026"; private static final String SEMESTER = "20"; private final SejongPublicData publicData; LectureSearchController(SejongPublicData publicData) { this.publicData = publicData; } @GetMapping("/departments") List departments() { return publicData.departments(UNDERGRADUATE_CATEGORY, YEAR, SEMESTER); } @GetMapping("/lectures") List lectures(@RequestParam(defaultValue = "") String department, @RequestParam(defaultValue = "") String name) { if (department.isBlank() && name.isBlank()) { // no condition returns the whole term (several MB), so this server requires one return List.of(); } return publicData.lectures(UNDERGRADUATE_CATEGORY, YEAR, SEMESTER, department, name, ""); } } ``` **Python** A standard library server. ```python import json from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs, urlparse from sejongpeia import SejongPublicData public_data = SejongPublicData() # one per process so the cache is shared UNDERGRADUATE_CATEGORY = "20" # the school's organization category code (list with organization_categories()) YEAR, SEMESTER = "2026", "20" # list terms with terms(UNDERGRADUATE_CATEGORY) def search_lectures(department_code: str, course_name: str) -> list: lectures = public_data.lectures(UNDERGRADUATE_CATEGORY, YEAR, SEMESTER, department_code=department_code, course_name=course_name) return [ {"course": lecture.name, "department": lecture.department_name, "schedule": lecture.schedule, "credits": lecture.credits, "course_number": lecture.course_number} for lecture in lectures ] class LectureSearchHandler(BaseHTTPRequestHandler): def do_GET(self): url = urlparse(self.path) if url.path == "/departments": departments = public_data.departments(UNDERGRADUATE_CATEGORY, YEAR, SEMESTER) body = [{"code": department.code, "college": department.college_name, "name": department.major_name or department.unit_name} for department in departments] elif url.path == "/lectures": query = parse_qs(url.query) department_code = query.get("department", [""])[0].strip() course_name = query.get("name", [""])[0].strip() if not department_code and not course_name: # no condition returns the whole term (several MB), so this server requires one self.send_error(400, "department or name is required") return body = search_lectures(department_code, course_name) else: self.send_error(404) return payload = json.dumps(body, ensure_ascii=False).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() self.wfile.write(payload) if __name__ == "__main__": HTTPServer(("127.0.0.1", 8000), LectureSearchHandler).serve_forever() ``` **Node.js** ```js import { createServer } from 'node:http'; import { SejongPublicData } from 'pro-sejongpeia'; const publicData = new SejongPublicData(); // one per process so the cache is shared const UNDERGRADUATE_CATEGORY = '20'; // the school's organization category code (list with organizationCategories()) const YEAR = '2026'; const SEMESTER = '20'; // list terms with terms(UNDERGRADUATE_CATEGORY) async function searchLectures(departmentCode, courseName) { const lectures = await publicData.lectures({ categoryCode: UNDERGRADUATE_CATEGORY, year: YEAR, semesterCode: SEMESTER, departmentCode, courseName, }); return lectures.map((lecture) => ({ course: lecture.name, department: lecture.departmentName, schedule: lecture.schedule, credits: lecture.credits, courseNumber: lecture.courseNumber, })); } createServer(async (request, response) => { const url = new URL(request.url, 'http://localhost'); let body; if (url.pathname === '/departments') { const departments = await publicData.departments({ categoryCode: UNDERGRADUATE_CATEGORY, year: YEAR, semesterCode: SEMESTER }); body = departments.map((department) => ({ code: department.code, college: department.collegeName, name: department.majorName || department.unitName })); } else if (url.pathname === '/lectures') { const departmentCode = (url.searchParams.get('department') ?? '').trim(); const courseName = (url.searchParams.get('name') ?? '').trim(); if (!departmentCode && !courseName) { // no condition returns the whole term (several MB), so this server requires one response.writeHead(400).end('department or name is required'); return; } body = await searchLectures(departmentCode, courseName); } else { response.writeHead(404).end(); return; } response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }).end(JSON.stringify(body)); }).listen(8000, '127.0.0.1'); ``` --- # Academic calendar widget Source: https://cassiiopeia.github.io/pro-sejongpeia/en/guides/academic-calendar/ Section: Guides Shows the undergraduate academic events within the next 30 days, ordered by start date. No login is needed. ## Request parameters Names follow Python `snake_case`; Node.js takes a `camelCase` options object (`{ start, end }`) and Java takes positional arguments. No login is needed. | Method | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---|---| | `calendar` | `start` | string | Yes | | Start date (`yyyy-MM-dd`) | | | `end` | string | Yes | | End date (`yyyy-MM-dd`) | | | `category_code` | string | No | `0001` | Category code (`0001` undergraduate, `0002` to `0007` graduate) | | `graduate_calendar` | `site_id` | string | Yes | | Graduate site name (for example `graduate`). Passed by the caller, so a new graduate school needs no release | | | `year` / `month` | integer | Yes | | Year / month to fetch | ## Response data structure One entry of the list `calendar(start, end)` returns. Field names are Python `snake_case`; Node.js uses `camelCase` (`start_date` -> `startDate`) and Java uses getters (`getStartDate()`). Values only show the shape. ```json [ { "year": "2026", "month": "3", "category_code": "0001", "category_name": "학부", "title": "2학기 중간고사 성적 입력", "title_en": "Grades input for Fall semester Midterm Examination", "start_date": "2026-03-02", "end_date": "2026-03-08", "extra": {} } ] ``` | Field | Meaning | |---|---| | `title` / `title_en` | Title / English title (school text) | | `start_date` / `end_date` | Start / end date (`yyyy-MM-dd` for undergraduate; same value for a one-day event) | | `year` / `month` | Year / month the entry belongs to | | `category_code` / `category_name` | Category code / name (`0001` is undergraduate) | | `extra` | Other school fields (empty for undergraduate events) | All fields: [Academic calendar](https://cassiiopeia.github.io/pro-sejongpeia/en/data/calendar/#response-data-structure). **On failure**: when the school server fails you get the last cached value or an empty list, without an exception. It looks the same as a period with no events. ## Good to know - Date formats differ: `yyyy-MM-dd` for undergraduate, `MM.dd` for graduate sites. [Academic calendar](https://cassiiopeia.github.io/pro-sejongpeia/en/data/calendar/) - Opening the widget does not hit the school each time. The same arguments are cached for 24 hours. [Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/) - Titles are the school's own text (Korean). The English title is `title_en` / `titleEn`. ## Example code **Java** ```java import java.time.LocalDate; import java.util.List; import kr.suhsaechan.sejongpeia.publicdata.SejongPublicData; import kr.suhsaechan.sejongpeia.publicdata.model.CalendarEvent; class AcademicCalendarWidget { static List upcomingAcademicEvents(SejongPublicData publicData, int daysAhead) { LocalDate today = LocalDate.now(); return publicData.calendar(today.toString(), today.plusDays(daysAhead).toString()).stream() .sorted(java.util.Comparator.comparing(CalendarEvent::getStartDate)) .toList(); } } ``` **Python** ```python from datetime import date, timedelta from sejongpeia import SejongPublicData public_data = SejongPublicData() def upcoming_academic_events(days_ahead: int = 30) -> list: """Undergraduate events that overlap the next days_ahead days (by start date)""" today = date.today() events = public_data.calendar(today.isoformat(), (today + timedelta(days=days_ahead)).isoformat()) # Dates are yyyy-MM-dd strings from the school, so string order equals date order return sorted(events, key=lambda event: event.start_date) if __name__ == "__main__": for event in upcoming_academic_events(): period = event.start_date if event.start_date == event.end_date else f"{event.start_date} ~ {event.end_date}" print(f"{period} {event.title}") ``` **Node.js** ```js import { SejongPublicData } from 'pro-sejongpeia'; const publicData = new SejongPublicData(); /** Undergraduate events that overlap the next daysAhead days (by start date) */ export async function upcomingAcademicEvents(daysAhead = 30) { const toDateText = (date) => date.toLocaleDateString('sv-SE'); // yyyy-MM-dd (local date) const today = new Date(); const end = new Date(today.getTime() + daysAhead * 24 * 60 * 60 * 1000); const events = await publicData.calendar({ start: toDateText(today), end: toDateText(end) }); // Dates are yyyy-MM-dd strings from the school, so string order equals date order return events.sort((first, second) => first.startDate.localeCompare(second.startDate)); } for (const event of await upcomingAcademicEvents()) { const period = event.startDate === event.endDate ? event.startDate : `${event.startDate} ~ ${event.endDate}`; console.log(`${period} ${event.title}`); } ``` --- # Dormitory and cafeteria bot Source: https://cassiiopeia.github.io/pro-sejongpeia/en/guides/dormitory-cafeteria-bot/ Section: Guides 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 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 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 ```json [ { "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 ```json [ { "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`) ```json { "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](https://cassiiopeia.github.io/pro-sejongpeia/en/data/dormitory/#response-data-structure) and [Cafeterias](https://cassiiopeia.github.io/pro-sejongpeia/en/data/cafeteria/#response-data-structure). **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 - 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](https://cassiiopeia.github.io/pro-sejongpeia/en/data/cafeteria/) - 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](https://cassiiopeia.github.io/pro-sejongpeia/en/data/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 **Java** ```java 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 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 items = publicData.cafeteriaMenu(placeId); if (items.isEmpty()) { return placeId + "번 식당: 메뉴 없음"; } List todaysItems = items.stream() .filter(item -> item.getDate().isEmpty() || item.getDate().equals(today)).toList(); List 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(); } } ``` **Python** ```python 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()) ``` **Node.js** ```js import { SejongPublicData } from 'pro-sejongpeia'; const publicData = new SejongPublicData(); const CAFETERIA_PLACE_IDS = ['1', '3', '4', '5', '6']; // the school's cafeteria numbers, may change const 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()); ``` --- # Using with coding agents Source: https://cassiiopeia.github.io/pro-sejongpeia/en/guides/agents/ Section: Guides So that coding agents such as Claude Code, Cursor, Codex and Copilot use this library correctly, the site builds agent-friendly files on every build. An agent that contributes to this repository should read [`AGENTS.md`](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/AGENTS.md) at the repository root. ## Files to give an agent | File | What it holds | Use it when | |---|---|---| | [`/llms.txt`](https://cassiiopeia.github.io/pro-sejongpeia/llms.txt) | Doc index with one-line descriptions (Korean list first, then English) | The agent should pick and read the docs it needs | | [`/api.json`](https://cassiiopeia.github.io/pro-sejongpeia/api.json) | Every public method: names in three languages, parameters and defaults, return model fields, failure behavior, cache, login requirement | The agent should write code now (most exact and shortest) | | [`/llms-full.en.txt`](https://cassiiopeia.github.io/pro-sejongpeia/llms-full.en.txt) | All English docs in one file (Korean: [`/llms-full.txt`](https://cassiiopeia.github.io/pro-sejongpeia/llms-full.txt)) | The environment cannot follow links, so paste everything | | [`AGENTS.md`](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/AGENTS.md) | Repository layout, dependency rule, test commands, how to add a data source | Changing this repository | The three files are generated when the docs site is built, so they always match the site. There is one source for each: `api.json` comes from `spec/api-index.json` in the repository, the two `llms` files come from this site's docs. CI fails if `spec/api-index.json` drifts from the three implementations. ## Prompt examples Tell the agent the file addresses first, then say what to build in one sentence. ### Example 1. Add student verification to a Spring app ```text Read https://cassiiopeia.github.io/pro-sejongpeia/api.json and https://cassiiopeia.github.io/pro-sejongpeia/en/guides/student-verification/ , then add Sejong student verification to the club sign-up API of my Spring Boot app. - Add the pro-sejongpeia dependency pinned to libraryVersion from api.json - Check only the student id and affiliation from the authenticate result; never store the password or write it to a log - Branch on SejongAuthException errorCode when responding (do not retry a wrong password) - In tests do not call the real portal; replace SuhSejongAuthEngine with a mock ``` ### Example 2. Show this semester's lectures in a Python bot ```text Read https://cassiiopeia.github.io/pro-sejongpeia/llms.txt , read the docs you need, then add a "/lectures department course" command to my Discord bot. Use SejongPublicData from pro-sejongpeia to find this semester's offered lectures (no login needed). - Do not hard-code department or semester codes; look them up with departments and terms - On an empty list answer "could not load, or no results" - An empty department returns the whole term (thousands of rows), so always pass a department code ``` ## Good to know - **Pin the version.** This library does not promise backward compatibility. Tell the agent to use `libraryVersion` from `api.json` and it installs the same version the docs describe. See [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/). - **Do not put a student id or password in a prompt.** It stays in the agent's history. If a test account is needed, name the environment variables and set the values yourself. Also check that the generated code never writes the password to a log or an exception message. - **Log in from a server only.** If the agent writes code that calls the portal from a browser or an app, have it redo that. The reason is in [Authentication methods](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/methods/). - **Check login-free data against the real school once.** Invented field names are the common mistake. `python3 scripts/probe-public-data.py` checks the response shape of the school's public data without an account (when you have the repository). Try the login part once with your own account; several wrong passwords can lock it ([Login verdict and account lock prevention](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/)). - **Values are the school's own text and empty when missing.** If the agent writes code that expects `null` or rewrites values, compare it with the [common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Reading `api.json` ```json { "libraryVersion": "…", "methods": [ { "id": "lectures", "group": "public_data", "requiresLogin": false, "names": { "python": "lectures", "node": "lectures", "java": ["lectures"] }, "usage": { "python": "lectures(category_code, year, …)", "node": "await lectures({ categoryCode, year, … })", "java": ["lectures(String categoryCode, …)"] }, "parameters": [{ "name": "category_code", "required": true, "…": "…" }], "returns": { "model": "Lecture", "isList": true, "fields": [{ "python": "course_number", "node": "courseNumber", "java": "courseNumber", "type": "string" }] }, "failure": { "mode": "returns-empty-value", "emptyValue": "[]", "invalidInput": null } } ], "models": { "Lecture": [{ "python": "course_number", "node": "courseNumber", "java": "courseNumber", "type": "string" }] } } ``` - `group` `public_data` needs no login; `auth` needs a student id and password. - `failure.mode` `returns-empty-value` means a school-side problem does not raise; you get the last cached value or an empty value. Only the cases in `invalidInput` raise. - Field names are Python `snake_case` and Node.js `camelCase`. Java models expose the same `camelCase` names as getters (`getCourseNumber()`). ## Read on - [Quick start](https://cassiiopeia.github.io/pro-sejongpeia/en/start/quickstart/): install and first call - [School data reference](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/): request, fields and cache per method - [Repository structure](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/structure/): the folders a contributing agent looks at first --- # Common rules Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/ Section: School data reference School data (public data) is what the school serves publicly, with no student ID or password. Java, Python and Node.js have the same methods, the same result fields and the same value rules. This page covers the rules every method shares. For each method, see its own page in the left menu. > **Unofficial endpoints** > This data comes from undocumented addresses that the school's own web screens call internally. The school can change or remove them without notice. When that happens, the [contract document (spec)](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) is updated first and all three languages are fixed in one PR. For the daily shape check, see [Daily live portal check](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/daily-live-check/). ## Client | Language | Entry point | Naming | |---|---|---| | Java | `kr.suhsaechan.sejongpeia.publicdata.SejongPublicData`, models in `kr.suhsaechan.sejongpeia.publicdata.model.*` | `camelCase` methods, fields read through getters | | Python | `sejongpeia.SejongPublicData` | `snake_case` methods and fields | | Node.js | the `SejongPublicData` class exported by the package | `camelCase`, many methods take one object | - Spring Boot auto-configuration registers a `SejongPublicData` bean. In plain Java, create it with `new SejongPublicData()`, after the [legacy TLS](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/legacy-tls/) setup. - Create **one instance per process**. The in-memory cache and the merging of identical requests live inside the instance. - Calling style: Java and Python are synchronous, Node.js returns a `Promise`. **Java** ```java // In Spring Boot, inject the bean instead SejongPublicData publicData = new SejongPublicData(); ``` **Python** ```python from sejongpeia import SejongPublicData public_data = SejongPublicData() # one per process so the cache is shared ``` **Node.js** ```javascript import { SejongPublicData } from 'pro-sejongpeia'; const publicData = new SejongPublicData(); // one per process so the cache is shared ``` ### Settings All optional. | Setting | Java (`sejongpeia.*`) | Python argument | Node.js option | Default | |---|---|---|---|---| | Request timeout (seconds) | `timeout-seconds` | `timeout_seconds` | `timeoutSeconds` | 10 | | Cache lifetime (seconds) | `public-data-cache-ttl-seconds` | `cache_ttl_seconds` | `cacheTtlSeconds` | 86400 | | Failure memory (seconds) | `public-data-failure-cache-seconds` | `failure_cache_seconds` | `failureCacheSeconds` | 60 | | Maximum cache entries | `public-data-cache-max-entries` | `cache_max_entries` | `cacheMaxEntries` | 500 | | Certificate verification | `ssl-verification` | `ssl_verification` | `sslVerification` | true | Java reads the `sejongpeia` prefix of `application.yml` (`SejongAuthProperties`). Without Spring, build the same properties object and pass it to `new SejongPublicData(properties)`. Settings shared with authentication are on [Configuration](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/configuration/). ## Methods | Data | Java | Python | Node.js | Page | |---|---|---|---|---| | Organization categories | `organizationCategories()` | `organization_categories()` | `organizationCategories()` | [Codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/) | | Terms | `terms(categoryCode)` | `terms(category_code)` | `terms(categoryCode)` | [Codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/) | | Departments | `departments(categoryCode, year, semesterCode)` | `departments(category_code, year, semester_code)` | `departments({ categoryCode, year, semesterCode })` | [Codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/) | | Course types | `courseTypes()` | `course_types()` | `courseTypes()` | [Codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/) | | Selective domains | `selectiveDomains()` | `selective_domains()` | `selectiveDomains()` | [Codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/) | | Course listings | `lectures(...)` | `lectures(...)` | `lectures({ ... })` | [Course listings](https://cassiiopeia.github.io/pro-sejongpeia/en/data/lectures/) | | Curriculum course | `curriculumCourses(departmentCode, courseNumber)` | `curriculum_courses(department_code, course_number)` | `curriculumCourses({ departmentCode, courseNumber })` | [Curriculum](https://cassiiopeia.github.io/pro-sejongpeia/en/data/curriculum/) | | Academic calendar | `calendar(...)` | `calendar(...)` | `calendar({ ... })` | [Calendar](https://cassiiopeia.github.io/pro-sejongpeia/en/data/calendar/) | | Graduate calendar | `graduateCalendar(siteId, year, month)` | `graduate_calendar(site_id, year, month)` | `graduateCalendar({ siteId, year, month })` | [Calendar](https://cassiiopeia.github.io/pro-sejongpeia/en/data/calendar/) | | Dormitory menu | `dormitoryMenu(...)` | `dormitory_menu(...)` | `dormitoryMenu(...)` | [Dormitory](https://cassiiopeia.github.io/pro-sejongpeia/en/data/dormitory/) | | Dormitory notices | `dormitoryNotices(...)` | `dormitory_notices(...)` | `dormitoryNotices({ ... })` | [Dormitory](https://cassiiopeia.github.io/pro-sejongpeia/en/data/dormitory/) | | Cafeteria menu | `cafeteriaMenu(placeId)` | `cafeteria_menu(place_id)` | `cafeteriaMenu(placeId)` | [Cafeterias](https://cassiiopeia.github.io/pro-sejongpeia/en/data/cafeteria/) | | Cafeteria list | `cafeteriaPlaces()` | `cafeteria_places()` | `cafeteriaPlaces()` | [Cafeterias](https://cassiiopeia.github.io/pro-sejongpeia/en/data/cafeteria/) | | Campus places | `campusPlaces(...)` | `campus_places(...)` | `campusPlaces(...)` | [Campus places](https://cassiiopeia.github.io/pro-sejongpeia/en/data/campus-places/) | | Extracurricular events | `extracurricularEvents(...)` | `extracurricular_events(...)` | `extracurricularEvents({ ... })` | [Extracurricular](https://cassiiopeia.github.io/pro-sejongpeia/en/data/extracurricular/) | ## Result value rules Identical in the three languages. Field mapping is defined by [`scripts/generate-public-data-fixtures.py`](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/scripts/generate-public-data-fixtures.py), and the tests compare against the same expected-result file ([`expected_public_data.json`](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/fixtures/public_data/expected_public_data.json)). - **School values as they are**: the library holds no code tables or name mappings. Department, term and cafeteria values change when the school changes them. Do not branch on lists of values. - **Missing values are empty strings**: a missing or `null` value becomes an empty string. Numeric fields also arrive as strings. The exceptions are `views` and `total` (integers) and `latitude` and `longitude` (floating point), which are `0` and `0.0` when missing. - **Rows are never dropped**: one odd row does not fail the whole list. Only a missing or non-array **container** (`data`, `items`, `events`, `root`, `dl_*`) is a failure. - **Scalars become text**: strings stay as they are, numbers use plain decimal form, booleans become `true` or `false`. - **`extra`**: school fields not used by the mapping, as a string-to-string map. Null and empty values and the deny list below are left out. Object or array values become compact JSON strings (no spaces). A field the school adds can show up in `extra` without a release. The dormitory models and the cafeteria menu item (`CafeteriaMenuItem`) have no `extra`. - **`extra` deny list**: `CD_ALIAS1` to `CD_ALIAS3` (and their `_ENG` variants), `regname`, `regid`, `STACK_TRACE`. Staff names never appear in `extra`. - **Field names**: Python uses `snake_case`, Node.js and Java use `camelCase`. Field tables in these docs use the Python names; read them as `camelCase` for Node.js and Java (for example `start_date` is `startDate`). ## The flow of one call > Diagram: A public data call returns the cache if it is under 24 hours old. Otherwise it sends one request to the school; on success it stores and returns the result, on failure it returns the last cached value or an empty list ## Failure isolation - A connection failure, timeout, non-2xx status, non-JSON body, missing expected key or `_SUBMIT_ERROR_` object returns the last cached value (even if expired), **without raising**. With no cache, it returns an empty list. Dormitory notices return an empty object (`total` 0, empty lists). - School server internals (such as `STACK_TRACE`) are never logged or returned. - **Only invalid input raises.** `curriculum_courses` with a blank department code or course number, and `graduate_calendar` with a malformed site name, raise `INVALID_INPUT` without sending a request. No other argument raises ([Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/)). - Authentication and public data do not depend on each other. A public data failure never changes an authentication result, and the other way around. - **An empty list has two meanings.** It cannot tell "no data" from "a problem on the school side". Show a message such as "could not load" next to it. ## Cache - Results are cached in memory per method and arguments. The default lifetime is 86400 seconds (one day). A fresh cache entry means no request. - After a failure with no cached value, the same call returns the empty value without a request for `failure_cache_seconds` (default 60). A stale cached value is still returned on failure. - The cache holds at most `cache_max_entries` (default 500) entries and evicts the oldest when full. The per-key locks go with their entries, so passing user-typed search text as an argument does not grow memory without bound. - **Concurrent calls with the same arguments share one request.** - One call makes one request. The exception is `cafeteria_places`, which sends one per cafeteria number, one after the other ([Cafeterias](https://cassiiopeia.github.io/pro-sejongpeia/en/data/cafeteria/)). - Be gentle with the school servers. A bot or widget only needs to refresh a few times a day. ## How requests are sent - Every request uses the library transport: the legacy TLS cipher the portal allows ([Legacy TLS](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/legacy-tls/)) and an identifying User-Agent (`pro-sejongpeia-/`, see [Firewall and User-Agent](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/firewall-user-agent/)). - SIS (`sjpt.sejong.ac.kr`) requests: JSON body (`{"dm_search": ...}`), headers `Content-Type: application/json; charset="UTF-8"` and `Accept: application/json`. - Form requests (dormitory, extracurricular): `application/x-www-form-urlencoded; charset=UTF-8` and `X-Requested-With: XMLHttpRequest`. - The dormitory (`happydorm`) responses are JSON with a `text/html` content type. The body is always parsed as JSON. - Requests are sent without login or cookies. ## Further reading - Contract sources: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md), [spec/REFERENCE_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/REFERENCE_DATA.md), and the overview of school data sources, [spec/REFERENCE_DATA_SURVEY.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/REFERENCE_DATA_SURVEY.md) - Usage examples: [Department and lecture search](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/department-lecture-search/), [Academic calendar widget](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/academic-calendar/), [Dormitory and cafeteria bot](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/dormitory-cafeteria-bot/) - Why failure and cache work this way: [Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/) - Adding a new source: [Add a new data source](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/add-data-source/) - Backward compatibility is not guaranteed, so pin your version: [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/) --- # Department and term codes Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/ Section: School data reference These methods return the code tables used by the lecture timetable screen of the school's student information system (SIS): organization categories, terms, departments, course types and selective domains. No login is needed. Do not hard-code codes or names: get them from these methods and pass them on to the others. Department codes use the same system as the `departmentCode` in a logged-in student's SIS result. Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Request parameters There are five methods: `organization_categories()`, `terms(category_code)`, `departments(category_code, year, semester_code)`, `course_types()`, `selective_domains()`. Node.js and Java use `camelCase` (`organizationCategories()`, `courseTypes()`, ...), and in Node.js `departments` takes an object: `departments({ categoryCode, year, semesterCode })`. | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `category_code` | string | required for `terms`, `departments` | - | Organization category code. Pass the `code` that `organization_categories()` returned, not a hard-coded list | | `year` | string | required for `departments` | - | Year. Pass the `year` that `terms(category_code)` returned | | `semester_code` | string | required for `departments` | - | Term code (table below) | The other three methods (`organization_categories`, `course_types`, `selective_domains`) take no parameters. | Term code | Term | |---|---| | `10` | 1st semester | | `11` | summer session | | `20` | 2nd semester | | `21` | winter session | **School request** - Endpoints under `POST https://sjpt.sejong.ac.kr/sch/sch/ext/SueOpenTimeQ/`: `doOnload.do` for organization categories, course types and selective domains, `doYearsmtlist.do` for terms, `doLesndeptgroup.do` for departments. The body is `{"dm_search": {...}}` JSON. - The department request's `dm_search` carries `ORGN_CLSF_CD`, `YEAR`, `SMT_CD` and `YEAR_SMT` (`/`, for example `year/20`). - Organization categories, course types and selective domains come from the same `doOnload.do` answer, but each method has its own cache entry. ## Response data structure All return lists (`OrganizationCategory`, `Term`, `Department`, `CourseType`, `SelectiveDomain`). In the example JSON, field names are Python `snake_case`; Node.js and Java use `camelCase` (for example `name_en` is `nameEn`). School data changes, so the values are examples. ```jsonc // terms("20") [{ "year": "2026", "semester_code": "20", "code": "2026/20", "name": "2026/2학기", "extra": {} }] // organization_categories() [{ "code": "20", "name": "학부", "name_en": "Undergraduate", "extra": { "PRT_ORD": "200" } }] // departments("20", "2026", "20") [{ "code": "3517", "level": "2", "college_code": "3500", "college_name": "인공지능융합대학", "unit_code": "3517", "unit_name": "AI로봇학과", "major_code": "", "major_name": "", "name_en": "College of AI Convergence Department of Artificial Intelligence and Robotics", "extra": { "DEPT_NM": "인공지능융합대학 AI로봇학과【3517 학부】", "ORGN_CLSF_CD": "20" } }] // course_types() [{ "code": "11", "name": "교필", "category_code": "20", "extra": { "CURI_TYPE_FULL_NM": "교양필수" } }] // selective_domains() [{ "code": "10", "name": "인성과도덕", "name_en": "Character & Morality", "extra": { "PRT_ORD": "1" } }] ``` Missing values are empty strings, and every model has `extra` (a string map). ### Organization category (`OrganizationCategory`) | Field | School key | Meaning | |---|---|---| | `code` | `COM_CD` | Category code (undergraduate `20`, general graduate school `30`, ...). Pass it as `category_code` to other methods | | `name`, `name_en` | `CD_NM`, `CD_NM_ENG` | Korean and English name | | `extra` | other fields | for example the display order `PRT_ORD` | The school's response has `CD_ALIAS1` to `CD_ALIAS3` fields, one of which holds a staff member's name. The library neither reads them nor puts them in `extra`. ### Term (`Term`) | Field | School key | Meaning | |---|---|---| | `year` | `YEAR` | Year (string) | | `semester_code` | `SMT_CD` | Term code (table above) | | `code` | `YEAR_SMT_CD` | In the form `/` | | `name` | `YEAR_SMT_NM` | Name as the school writes it | ### Department (`Department`) | Field | School key | Meaning | |---|---|---| | `code` | `DEPT_CD` | Department code, the same system as `departmentCode` in a student's SIS result | | `level` | `DEPT_LEVEL_CD` | `2` department or division, `3` major inside a division | | `college_code`, `college_name` | `DEPT_L_CD`, `DEPT_L_PRINT` | College code and clean name | | `unit_code`, `unit_name` | `DEPT_M_CD`, `DEPT_M_PRINT` | Department or division code and name | | `major_code`, `major_name` | `DEPT_S_CD`, `DEPT_S_PRINT` | Major code and name (only when `level` is `3`) | | `name_en` | `DEPT_NM_ENG` | English name (empty on some rows) | - When `level` is `2`, `code` equals `unit_code`. When it is `3`, `code` equals `major_code` and the parent is `unit_code`. - The `*_PRINT` names are clean. The `*_NM` fields that land in `extra` (such as `DEPT_NM`) carry a decoration like `【3517 학부】`, and the library does not strip it. - `college_code` is not always a real college. Interdisciplinary majors and legacy grouping codes can appear, so use the code only as a join key. - Every school column outside the fields above goes to `extra`. ### Course type (`CourseType`) and selective domain (`SelectiveDomain`) | Model | Fields | School keys | |---|---|---| | `CourseType` | `code`, `name`, `category_code` | `CURI_TYPE_CD`, `CURI_TYPE_CD_NM`, `ORGN_CLSF_CD` | | `SelectiveDomain` | `code`, `name`, `name_en` | `COM_CD`, `CD_NM`, `CD_NM_ENG` | These are codes with the same names as `course_type_code` and `domain_code` in course listings ([Course listings](https://cassiiopeia.github.io/pro-sejongpeia/en/data/lectures/)). - Cached per method and arguments for one day (86400 seconds by default). Departments are cached separately for each category, year and term, with one school request per call. - A connection failure, timeout, missing dataset key or a `_SUBMIT_ERROR_` answer from the school (the school can answer HTTP 200 with an error object) returns the last cached value or an empty list, without raising. - An empty list can be a valid answer: for example, some organization categories have no departments in a term. - These five methods never raise for input problems. ## Known limits - Unofficial, unversioned endpoints. The school can change or remove them without notice. - The department list differs by term. Departments are added and renamed, so always pass the term you want. - This list holds the departments that **offer courses** in that term (for the timetable filter). It is not every administrative unit: non-academic units such as centers and offices are not included. - That `code` matches the student's SIS department code holds for an undergraduate student account. Codes for graduate students, double majors and staff accounts may differ. - Contract source: [spec/REFERENCE_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/REFERENCE_DATA.md) ## Example code **Java** ```java List categories = publicData.organizationCategories(); List terms = publicData.terms("20"); // undergraduate terms List departments = publicData.departments("20", "2026", "20"); // undergraduate, term code 20 = 2nd semester ``` **Python** ```python categories = public_data.organization_categories() terms = public_data.terms("20") # undergraduate terms departments = public_data.departments("20", "2026", "20") # undergraduate, term code 20 = 2nd semester ``` **Node.js** ```javascript const categories = await publicData.organizationCategories(); const terms = await publicData.terms('20'); // undergraduate terms const departments = await publicData.departments({ categoryCode: '20', year: '2026', semesterCode: '20' }); // undergraduate, term code 20 = 2nd semester ``` For a full example that searches lectures from the department list, see [Department and lecture search](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/department-lecture-search/). --- # Course listings Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/lectures/ Section: School data reference Search a term's course listings by department code, course name and instructor name. No login is needed, and all three conditions are optional, so **with all of them empty you get the whole term**. Find department and term codes on [Department and term codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/). Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). > **At a glance** > - Method `lectures`, no login needed, returns a list of `Lecture`. > - With department, course name and instructor all empty you get the whole term. The response is large, so search by department when you can. > - When the school fails it returns the last cached value or an empty list instead of throwing. ## Request parameters Python uses `snake_case`; Node.js and Java use `camelCase` (for example `department_code` is `departmentCode`). In Java, pass an empty string (`""`) for the optional arguments you do not use. | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `category_code` | string | required | - | Organization category code (undergraduate `20`, general graduate school `30`, ...). Get the values from the [code tables](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/) and pass them in | | `year` | string | required | - | Year | | `semester_code` | string | required | - | Term code (`10`, `11`, `20`, `21`) | | `department_code` | string | optional | `""` | Department code. Empty means no department limit | | `course_name` | string | optional | `""` | Course name search text (partial match). Empty means no limit | | `instructor_name` | string | optional | `""` | Instructor name search text. Empty means no limit | - The three conditions are independent: combine only the ones you need. **With all three empty you get the whole term.** - Returns a list of `Lecture`. **School request** - `POST https://sjpt.sejong.ac.kr/sch/sch/ext/SueOpenTimeQ/doList.do`, body `{"dm_search": {...}}`, response dataset `dl_main`. - The school answers `_SUBMIT_ERROR_` if any filter key is missing, so the library sends every key you do not use as an empty value. - The school treats a single space as a search text, so values are trimmed before sending. ## Response data structure Example JSON. Field names are Python `snake_case`; Node.js and Java use `camelCase`. School data changes, so the values are examples. ```json [ { "course_number": "011839", "section": "001", "name": "전공탐색", "course_type_code": "1E", "course_type_name": "공필", "credits": "1.0", "hours": "1.0 / 0 / 1", "total_hours": "1", "schedule": "목19:00-20:00", "room": "센B104", "instructor": "홍길동", "grade_level": "1", "department_code": "3517", "department_name": "AI로봇학과", "year": "2026", "semester_code": "20", "extra": { "PLAN_REG_YN": "Y", "YEAR_SMT_NM": "2026/2학기" } } ] ``` All fields are strings, empty when missing. `extra` is a string map of the school fields not listed in the table below. | Field | School key | Meaning | |---|---|---| | `course_number` | `CURI_NO` | Course number | | `section` | `CLASS` | Section | | `name` | `CURI_NM` | Course name | | `course_type_code`, `course_type_name` | `CURI_TYPE_CD`, `CURI_TYPE_CD_NM` | Course type code and name | | `domain_code`, `domain_name` | `SLT_DOMAIN_CD`, `SLT_DOMAIN_CD_NM` | Selective domain code and name | | `credits` | `CDT` | Credits (for example `3.0`) | | `hours` | `TM_NUM` | Hours, as the school writes them (for example `3.0 / 3 / 0`) | | `total_hours` | `TOT_TM_NUM` | Total hours | | `schedule` | `LESN_TIME` | Class time (for example `화13:00-15:00`) | | `room` | `LESN_ROOM` | Classroom | | `instructor` | `LESN_EMP` | Instructor name | | `grade_level` | `STUDENT_YEAR` | Year level | | `language_code`, `language_name` | `CURI_LANG_CD`, `CURI_LANG_NM` | Teaching language | | `department_code`, `department_name` | `DEPT_CD`, `SCH_DEPT_ALIAS` | Offering department code and name | | `managing_department` | `MANAGE_DEPT_NM` | Managing department | | `remark` | `REMARK` | Remark | | `year`, `semester_code` | `YEAR`, `SMT_CD` | Year and term code | | `extra` | other fields | Original school fields | > **Instructor names are included** > `instructor` is the instructor name as the school publishes it in the timetable. If your service republishes the results as they are, choose the fields to expose yourself. The example in [Department and lecture search](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/department-lecture-search/) also passes on only the fields it needs. - Cached for one day (86400 seconds by default) per combination of conditions, with one school request per call. - School-side failures (connection, timeout, bad response, `_SUBMIT_ERROR_`) return the last cached value or an empty list. With no cache, the same call returns an empty list without a request for `failure_cache_seconds` (default 60). - `lectures` never raises because of its filter values. - An empty list cannot tell "no course matches" from "a problem on the school side". ## Known limits - Unofficial endpoint. Behavior such as the filter-key requirement changes if the school changes it. - The language filter is fixed to `LANG_CHK="KOR"`. - An answer with all conditions empty is several MB, and one cache entry stays in memory for `cache_ttl_seconds`. If you do not need everything, query one department at a time. - The instructor syllabus is not provided. Its form carries the instructor's name and contact lines, so it is left out of the default scope. The course overview is available as `overview` in [Curriculum](https://cassiiopeia.github.io/pro-sejongpeia/en/data/curriculum/). - Contract source: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) ## Example code **Java** ```java // Lectures of one department (term code 20 = 2nd semester, course name and instructor left empty) List departments = publicData.departments("20", "2026", "20"); List lectures = publicData.lectures("20", "2026", "20", departments.get(0).getCode(), "", ""); // Search by course name only List byName = publicData.lectures("20", "2026", "20", "", "알고리즘", ""); ``` **Python** ```python departments = public_data.departments("20", "2026", "20") lectures = public_data.lectures("20", "2026", "20", department_code=departments[0].code) # Search by course name only by_name = public_data.lectures("20", "2026", "20", course_name="알고리즘") ``` **Node.js** ```javascript const departments = await publicData.departments({ categoryCode: '20', year: '2026', semesterCode: '20' }); const lectures = await publicData.lectures({ categoryCode: '20', year: '2026', semesterCode: '20', departmentCode: departments[0].code }); // Search by course name only const byName = await publicData.lectures({ categoryCode: '20', year: '2026', semesterCode: '20', courseName: '알고리즘' }); ``` With a lecture's `course_number` and `department_code` you can look up the suggested year, term and course overview from that department's curriculum ([Curriculum](https://cassiiopeia.github.io/pro-sejongpeia/en/data/curriculum/)). --- # Academic calendar (undergraduate and graduate) Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/calendar/ Section: School data reference Give a date range and get the academic calendar with titles, start dates and end dates. No login is needed. Undergraduate and graduate schedules come from different school sources, so there are two methods. `calendar` reads the homepage calendar (undergraduate `0001`, graduate `0002` to `0007`); `graduate_calendar` reads the per-site graduate school calendars. Both return the same result model (`CalendarEvent`), but **the date formats differ.** For a widget that shows the next 30 days, see [Academic calendar widget](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/academic-calendar/). Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Request parameters | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `start` (`calendar`) | string `yyyy-MM-dd` | required | none | Start of the range | | `end` (`calendar`) | string `yyyy-MM-dd` | required | none | End of the range. Several months can be read in one call | | `category_code` (`calendar`) | string | optional | `0001` | Selects the category: `0001` undergraduate, `0002` to `0007` graduate. Check the category name in the result's `category_name` | | `site_id` (`graduate_calendar`) | string | required | none | The graduate school's site name (for example `graduate`, `gsi`, `mba`). The caller passes it, so a newly created graduate school needs no release. It becomes a host name, so it must match `[a-z0-9][a-z0-9-]{0,30}`, otherwise `INVALID_INPUT` with no request | | `year` (`graduate_calendar`) | integer | required | none | Year (Node.js accepts a number or a string) | | `month` (`graduate_calendar`) | integer | required | none | Month (Node.js accepts a number or a string) | - Naming: Python uses `snake_case`, Node.js and Java use `camelCase` (`categoryCode`, `siteId`). Node.js takes one object: `calendar({ start, end, categoryCode? })`, `graduateCalendar({ siteId, year, month })`. Java is `calendar(startDate, endDate)`, `calendar(startDate, endDate, categoryCode)` and `graduateCalendar(siteId, year, month)`; the two-argument version uses `0001`. - `calendar` school request: `GET https://www.sejong.ac.kr/kor/academics/academic-calendar.do?mode=getCalendarData&start=&end=&collDiv=`, answer array `data`. - `graduate_calendar` school request: `GET https://.sejong.ac.kr/_custom/sejong/_app/calendar-graduate/proc/getCalendarData.jsp?year=&month=&siteId=`. - `items` in the answer is a JSON string, so the library parses it once more (an array is accepted too). If `success` is not true or `items` is missing, it counts as a school-side failure. A month with no events is a normal answer. ## Response data structure Example of `calendar` (field names in Python style; Node.js and Java use `camelCase`). School data changes, so the values are examples. ```json [ { "year": "2026", "month": "3", "category_code": "0001", "category_name": "Undergraduate", "title": "Start of classes", "title_en": "Start of classes", "start_date": "2026-03-02", "end_date": "2026-03-02", "extra": {} } ] ``` Example of `graduate_calendar`. Dates are `MM.dd`, and `extra` carries the day of the week. ```json [ { "year": "2026", "month": "3", "category_code": "0002", "category_name": "Graduate school", "title": "Thesis submission", "title_en": "Thesis submission", "start_date": "03.04", "end_date": "03.10", "extra": { "frDtKo": "Wed", "toDtKo": "Tue" } } ] ``` All fields are strings, and a missing value is an empty string. | Field | School key | Meaning | |---|---|---| | `year`, `month` | `scmYear`, `scmMonth` | Year and month the event belongs to | | `category_code`, `category_name` | `collDiv`, `collDivNm` | Category code and name (for example `0001`) | | `title` | `scmSubject` | Title (school's own text) | | `title_en` | `scmSubjectEng` | English title | | `start_date`, `end_date` | `frDt`, `toDt` | Start and end date | | `extra` | other fields | For example `frDtKo`, `toDtKo` of `graduate_calendar` (day of the week of each date) | ### Date format difference | Method | `start_date` / `end_date` format | Note | |---|---|---| | `calendar` | `yyyy-MM-dd` | String order equals date order | | `graduate_calendar` | `MM.dd` | The year comes from the row's `year`; a range crossing year end is not expanded | Convert them yourself when merging both into one list. The library does not convert. ### Failure behaviour - Cached for one day (86400 seconds by default) per method, category or site, and range. One call sends one request. - A school failure (connection, timeout, bad answer, missing expected key) returns the last cached value or an empty list. With no cache, it returns an empty list without a request for 60 seconds (default). - The only exception is a malformed site name in `graduate_calendar` (`INVALID_INPUT`). `calendar` never raises. - An empty list cannot tell "no events in that range" from "a problem on the school side". ## Known limits - Titles are the school's own text. If the school changes wording they change, so do not branch on titles. - `graduate_calendar` returns only `MM.dd`, so the end year of an event crossing year end is unknown. - Which graduate school each `calendar` code `0002` to `0007` stands for, check in the result's `category_name`. - The sites share a similar screen, but answers can differ per site, so try your own `site_id`. - Business periods that need login are not part of these methods ([Business periods](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/business-periods/)). - Contract source: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) ## Example code **Java** ```java // Undergraduate calendar (yyyy-MM-dd) List events = publicData.calendar("2026-10-30", "2026-11-30"); // Graduate site calendar (site name, year, month) List graduateEvents = publicData.graduateCalendar("graduate", 2026, 10); ``` **Python** ```python events = public_data.calendar("2026-10-30", "2026-11-30") # undergraduate (category_code defaults to 0001) # Graduate site calendar (site name, year, month) graduate_events = public_data.graduate_calendar("graduate", 2026, 10) ``` **Node.js** ```javascript const events = await publicData.calendar({ start: '2026-10-30', end: '2026-11-30' }); // undergraduate // Graduate site calendar (site name, year, month) const graduateEvents = await publicData.graduateCalendar({ siteId: 'graduate', year: 2026, month: 10 }); ``` --- # Dormitory Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/dormitory/ Section: School data reference The weekly menu and the notice list that the dormitory site (`happydorm.sejong.ac.kr`) serves publicly. No login is needed. For an example that turns the menu into a message, see [Dormitory and cafeteria bot](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/dormitory-cafeteria-bot/). Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Request parameters | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `location` (`dormitory_menu`, `dormitory_notices`) | string | optional | `SJ` | Dormitory location value | | `page` (`dormitory_notices`) | integer | optional | `1` | Notice page number | | `rows` (`dormitory_notices`) | integer | optional | `10` | Notices per page | | `keyword` (`dormitory_notices`) | string | optional | `""` | Title search text. Empty returns all | - Naming: Python uses `snake_case`, Node.js and Java use `camelCase`. Node.js takes `dormitoryMenu(location = 'SJ')` and `dormitoryNotices({ page, rows, keyword, location })`. Java has `dormitoryMenu()` and `dormitoryNotices()` without the optional arguments, and the full `dormitoryMenu(String location)` and `dormitoryNotices(int page, int rows, String keyword, String location)`; pass `""` for no search text. - School request (menu): `POST https://happydorm.sejong.ac.kr/food/indexFoodList.do`, form `locgbn=`. - School request (notices): `POST https://happydorm.sejong.ac.kr/bbs/getBbsList.do`, form `cPage`, `rows`, `bbs_locgbn`, `bbs_id=notice`, `sType=subject`, `sWord`. - The answer is JSON with a `text/html` content type, so the library always reads the body as JSON. ## Response data structure `dormitory_menu` returns a list of `DormitoryMenuDay` (7 days) (field names in Python style; Node.js and Java use `camelCase`). School data changes, so the values are examples. ```json [ { "date": "2026-03-02", "breakfast": "rice soup side dishes", "lunch": "rice soup main dish kimchi", "dinner": "rice soup main dish kimchi", "alternative": "light meal exchange" } ] ``` `dormitory_notices` returns one `DormitoryNotices` object. ```json { "total": 120, "pinned": [ { "id": "1001", "title": "Pinned notice title", "date": "2026-03-01", "views": 300, "file_name": "guide.png" } ], "items": [ { "id": "1000", "title": "Regular notice title", "date": "2026-02-28", "views": 80, "file_name": "" } ] } ``` The dormitory models have no `extra`. A missing value is an empty string, and `views` and `total` are `0`. ### Menu (`DormitoryMenuDay`) | Field | School key | Meaning | |---|---|---| | `date` | `fo_date1` to `7` | Date | | `breakfast` | `fo_menu_mor1` to `7` | Breakfast | | `lunch` | `fo_menu_lun1` to `7` | Lunch | | `dinner` | `fo_menu_eve1` to `7` | Dinner | | `alternative` | `fo_sub_menu1` to `7` | Alternative menu | Menu strings are the school's own text, and one cell can hold several dishes strung together. The library does not split them. ### Notices (`DormitoryNotices` and `DormitoryNotice`) | Field | School key | Meaning | |---|---|---| | `total` | `totalCount[0].cnt` | Total count (integer) | | `pinned` | `topList` | Pinned notices | | `items` | `list` | Regular notices | One notice: | Field | School key | Meaning | |---|---|---| | `id` | `seq` | Notice number | | `title` | `subject` | Title | | `date` | `regdate` | Posted date | | `views` | `visitcnt` | View count (integer) | | `file_name` | `file_name1` | Attached file name (empty if none) | The author fields (`regname`, `regid`) are not read and are not in the result. ### Failure behaviour - Cached for one day (86400 seconds by default) per method and arguments (location, page, rows, keyword). One call sends one request. - A school failure returns the last cached value; with none, the menu is an empty list and the notices are an object with `total` 0 and empty lists. Invalid input does not raise. - If the `root` array is missing or empty in the answer, both the menu and the notices fail. - Notices only: if `topList`, `list` or `totalCount` is missing, it is treated as "no results", not an outage, and returns empty lists with `total` 0. This keeps a search with no matching notice from counting as an outage. ## Known limits - The default `location` `SJ` is the supported value. For another value, try a real call first. - On weekends, before the school posts the menu, today's date may be missing from the list. Handle it in the caller ([Dormitory and cafeteria bot](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/dormitory-cafeteria-bot/) shows the first day instead). - An empty list cannot tell "no notices or menu" from "a problem on the school side". - Notice bodies are not provided. You get only title, date, view count and attached file name. - Contract source: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) ## Example code **Java** ```java List menuDays = publicData.dormitoryMenu(); // 7 days DormitoryNotices notices = publicData.dormitoryNotices(1, 3, "", "SJ"); // page 1, 3 rows ``` **Python** ```python menu_days = public_data.dormitory_menu() # 7 days notices = public_data.dormitory_notices(rows=3) # latest 3 for notice in notices.items: print(notice.date, notice.title) ``` **Node.js** ```javascript const menuDays = await publicData.dormitoryMenu(); // 7 days const notices = await publicData.dormitoryNotices({ rows: 3 }); // latest 3 ``` --- # Cafeterias Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/cafeteria/ Section: School data reference Get a cafeteria's menu and prices from the school's own cafeteria number (`cafeteria_menu`), and a list of cafeteria numbers, names, buildings and locations (`cafeteria_places`). No login is needed. The school has no cafeteria list API, so the list is built by the library: it calls the menu answer for numbers 1 to `max_place_id` one after the other and collects the places. Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Request parameters | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `place_id` (`cafeteria_menu`) | string | required | none | The school's cafeteria number. The library has no number table, so check `cafeteria_places()` | | `max_place_id` (`cafeteria_places`) | integer | optional | `10` | Number range `1` to `max_place_id` used to build the cafeteria list. Raise it when cafeterias are added (max `50`; outside the range it is `INVALID_INPUT` with no request) | - Naming: Python uses `snake_case`, Node.js and Java use `camelCase` (`maxPlaceId`). Node.js is `cafeteriaMenu(placeId)` and `cafeteriaPlaces({ maxPlaceId })`; Java is `cafeteriaMenu(String placeId)`, `cafeteriaPlaces()` and `cafeteriaPlaces(int maxPlaceId)`. - School request: `GET https://www.sejong.ac.kr/kor/unilife/cafeteria-info.do?mode=getMenuList&placeId=`, answer array `items`. - `cafeteria_places` calls the same address for numbers 1 to `max_place_id`, one at a time (no parallel requests). It is the only method where one call sends several requests. - For each number, one cafeteria is built from the first row of the answer. A number with no menu rows is not added. A `place_id` appears once, and the result is in number order. ## Response data structure Example of `cafeteria_menu` (field names in Python style; Node.js and Java use `camelCase`). School data changes, so the values are examples. ```json [ { "place_id": "1", "place_name": "Cafeteria name", "building_name": "Building name", "location": "6F", "date": "2026-03-02", "day": "Mon", "meal_type_id": "1", "meal_name": "Lunch", "menu_name": "soup · main dish · kimchi", "price": "7000" }, { "place_id": "3", "place_name": "Another cafeteria", "building_name": "Building name", "location": "B1", "date": "", "day": "", "meal_type_id": "", "meal_name": "", "menu_name": "ramen", "price": "3300" } ] ``` Example of `cafeteria_places`: ```json [ { "place_id": "1", "place_name": "Cafeteria name", "building_name": "Building name", "location": "6F", "extra": {} } ] ``` All fields are strings, and a missing value is an empty string. Depending on the cafeteria, the menu is either a dated weekly menu or a standing menu with no date. Rows of a standing-menu cafeteria have empty `date`, `day`, `meal_type_id` and `meal_name`. ### Menu (`CafeteriaMenuItem`, no `extra`) | Field | School key | Meaning | |---|---|---| | `place_id` | `placeId` | Cafeteria number | | `place_name` | `placeName` | Cafeteria name | | `building_name` | `buildingName` | Building | | `location` | `location` | Location (for example `6F`) | | `date` | `menuDate` | Menu date (empty for standing-menu cafeterias) | | `day` | `day` | Day of the week (empty for standing-menu cafeterias) | | `meal_type_id` | `mealTypeId` | Meal type number (`1` lunch, `2` dinner) | | `meal_name` | `mealName` | Meal name (school's own text) | | `menu_name` | `menuName` | Menu name | | `price` | `price` | Price (string, for example `5500`) | The library does not translate meal type numbers, so use the `meal_name` the school gives. ### Cafeteria (`CafeteriaPlace`) | Field | Meaning | |---|---| | `place_id`, `place_name` | Cafeteria number, name | | `building_name`, `location` | Building, location | | `extra` | The remaining school fields, without the menu fields (date, day, meal, menu name, price) | ### Failure behaviour - Cached per number for one day (86400 seconds by default). A school failure returns the last cached value, or an empty list. A failure with no cache returns an empty list without a request for 60 seconds (default). - If any one request of `cafeteria_places` fails, the whole list fails. A partial list is never cached, so only part of the list cannot stay cached for a day. - Neither method raises for school-side problems. Only a `max_place_id` outside the range raises `INVALID_INPUT`. - An empty `cafeteria_menu` does not prove the number is wrong. A number that does not exist and a day with no menu posted yet (holidays included) both return an empty list. ## Known limits - The school has no cafeteria list API, so **a cafeteria whose number is above `max_place_id` is not found.** Raise `max_place_id` when cafeterias are added. - A cafeteria with an empty menu (weekly menu not posted, holiday) is missing from the list until the next refresh (one day by default). - A cafeteria with no menu number cannot appear in this list. - Cafeteria numbers can change, so check `cafeteria_places()` instead of hard-coding numbers. The library does not guess names from numbers. - Contract source: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) ## Example code **Java** ```java List places = publicData.cafeteriaPlaces(); // 10 requests by default (maxPlaceId raises it to 50), cached for a day List menu = publicData.cafeteriaMenu("1"); // menu of cafeteria 1 ``` **Python** ```python places = public_data.cafeteria_places() # 10 requests by default (max_place_id raises it to 50), cached for a day menu = public_data.cafeteria_menu("1") # menu of cafeteria 1 ``` **Node.js** ```javascript const places = await publicData.cafeteriaPlaces(); // 10 requests by default (maxPlaceId raises it to 50), cached for a day const menu = await publicData.cafeteriaMenu('1'); // menu of cafeteria 1 ``` --- # Campus places Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/campus-places/ Section: School data reference Get the place list used by the school's campus map, without login. It has names, latitude and longitude and guide information for buildings and facilities, so you can use it directly in a map app or a campus guide service. Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Request parameters Python uses `snake_case` (`campus_places`); Node.js and Java use `camelCase` (`campusPlaces`). Java has two forms, `campusPlaces()` and `campusPlaces(String placeType)`; Node.js is `campusPlaces(placeType = '')`. | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `place_type` | string | optional | `""` | The school's place type value. It is not a fixed list: pass a `place_type` value from the results to filter, or leave it empty for no type limit | - Returns a list of `CampusPlace`. **School request** - `GET https://www.sejong.ac.kr/kor/intro/campus-map.do?mode=getPlaceUserList&placeType=`, response array `items`. ## Response data structure Example JSON. Field names are Python `snake_case`; Node.js and Java use `camelCase`. School data changes, so the values are examples. ```json [ { "id": "3", "name": "산들푸드", "name_en": "", "place_type": "WELFARE", "category": "식당", "latitude": 37.5495931628, "longitude": 127.0751638599, "info": "학생회관 B1층", "phone": "02-000-0000", "description": "", "extra": { "orderNo": "1", "useYn": "Y" } } ] ``` Missing values are empty strings; latitude and longitude are numbers (`0.0` when missing or unreadable). | Field | School key | Meaning | |---|---|---| | `id` | `id` | Place number. A different system from the cafeteria numbers ([Cafeteria](https://cassiiopeia.github.io/pro-sejongpeia/en/data/cafeteria/)) | | `name`, `name_en` | `placeName`, `placeNameEn` | Korean and English name | | `place_type` | `placeType` | Place type (for example `BUILDING`, `WELFARE`) | | `category` | `welfareCategory` | Facility category | | `latitude`, `longitude` | `latitude`, `longitude` | Latitude, longitude (floating point) | | `info` | `placeInfo` | Guide information | | `phone` | `telNum` | Phone number | | `description` | `description` | Description | | `extra` | other fields | Original school fields | - Cached for one day (86400 seconds by default) per type value, with one school request per call. - School-side failures (connection, timeout, bad response, missing `items`) return the last cached value or an empty list. With no cache, an empty list is returned without a request for 60 seconds (default). - This method never raises. - An empty list cannot tell "no places" from "a problem on the school side". ## Known limits - The place `id` is a different system from the cafeteria numbers in the menu answers, and nothing links the two. - A missing coordinate is `0.0`, so check before you plot a point. - `phone` and `info` are the values the school entered. Review them before you pass them on to outside users. - Contract source: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) ## Example code **Java** ```java List places = publicData.campusPlaces(); for (CampusPlace place : places) { System.out.println(place.getName() + " " + place.getLatitude() + "," + place.getLongitude()); } ``` **Python** ```python places = public_data.campus_places() for place in places: print(place.name, place.latitude, place.longitude) ``` **Node.js** ```javascript const places = await publicData.campusPlaces(); for (const place of places) console.log(place.name, place.latitude, place.longitude); ``` --- # Extracurricular programs Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/extracurricular/ Section: School data reference Query the schedule that the school's extracurricular program portal (`do.sejong.ac.kr`) serves publicly, by date range. No login is needed. It includes the category, start and end time, location, description and detail address. Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Request parameters | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `start` | string `yyyy-MM-dd` | required | none | Start of the range | | `end` | string `yyyy-MM-dd` | required | none | End of the range | | `category` | string | optional | `""` | The school's category value. Empty returns all. Check category names in the result's `category_title` | - Naming: Python uses `snake_case`, Node.js and Java use `camelCase`. Node.js is `extracurricularEvents({ start, end, category = '' })`; Java is `extracurricularEvents(startDate, endDate)` and `extracurricularEvents(startDate, endDate, category)` (pass `""` to leave the optional one empty). - School request: `POST https://do.sejong.ac.kr/ko/process/schedule/getEvents`, form `cid=eco`, `category`, `start`, `end`, `start_year`, `end_year` (the year fields are the first four characters of each date), answer array `events`. - Form request headers are `application/x-www-form-urlencoded; charset=UTF-8` and `X-Requested-With: XMLHttpRequest`. ## Response data structure A list of `ExtracurricularEvent` (field names in Python style; Node.js and Java use `camelCase`). School data changes, so the values are examples. ```json [ { "id": "event-id", "title": "Program title", "start": "2026-03-10T14:00:00+09:00", "end": "2026-03-10T16:00:00+09:00", "category_title": "Category name", "url": "https://do.sejong.ac.kr/ko/program/all/view/0000", "description": "Program description", "location": "Place" } ] ``` All are strings, and a missing value is an empty string. This model has no `extra`. | Field | School key | Meaning | |---|---|---| | `id` | `id` | Program number | | `title` | `title` | Title | | `start`, `end` | `start`, `end` | Start and end (date-time strings, school's own text) | | `category_title` | `category_title` | Category name | | `url` | `data.url` | Detail address | | `description` | `data.description` | Description | | `location` | `data.location` | Place | `url`, `description` and `location` are read from the `data` object of the school answer. ### Failure behaviour - Cached for one day (86400 seconds by default) per range and category. One call sends one request. - A school failure (connection, timeout, bad answer, no `events`) returns the last cached value or an empty list. With no cache, it returns an empty list without a request for 60 seconds (default). - This method never raises. - An empty list cannot tell "no programs in that range" from "a problem on the school side". ## Known limits - No list of usable `category` values is provided. Check category names in the result's `category_title`. - `start` and `end` are in the school's own format, so check real values before parsing. - The maximum range of one call is not specified, so try a real call before using a long range. - Contract source: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) ## Example code **Java** ```java List events = publicData.extracurricularEvents("2026-10-01", "2026-10-31"); ``` **Python** ```python events = public_data.extracurricular_events("2026-10-01", "2026-10-31") for event in events: print(event.start, event.title, event.location) ``` **Node.js** ```javascript const events = await publicData.extracurricularEvents({ start: '2026-10-01', end: '2026-10-31' }); ``` --- # Curriculum Source: https://cassiiopeia.github.io/pro-sejongpeia/en/data/curriculum/ Section: School data reference With a department code and a course number taken from the course listings, get the suggested year and term, course type, credits, hours and the **course overview** from that department's curriculum. No login is needed, and you look up one course at a time (there is no whole-curriculum list). Common rules are on [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Request parameters Python uses `snake_case` (`curriculum_courses`); Node.js and Java use `camelCase` (`curriculumCourses`). Node.js takes an object: `curriculumCourses({ departmentCode, courseNumber })`. | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `department_code` | string | required | - | SIS code (`DEPT_CD`) of a degree department. Get it from the [department list](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/) or from `department_code` in a course result and pass it in | | `course_number` | string | required | - | Course number. Pass `course_number` (`CURI_NO`) from a course result | - Returns a list of `CurriculumCourse` (0 or 1 items for one course). - **An empty department code or course number raises `INVALID_INPUT`** and no request is sent ([Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/)). **School request** - `GET https://www.sejong.ac.kr/curriculum/getCurriculumOneKor.do?curiNo=&deptNo=`, response array `items`. - Without an `Accept: application/json` header the school answers 404, so all three languages send it. - `deptCd` in the curriculum answer uses the same code system as `DEPT_CD` in the SIS. ## Response data structure Example JSON. Field names are Python `snake_case`; Node.js and Java use `camelCase`. School data changes, so the values are examples. ```json [ { "department_code": "3517", "department_name": "AI로봇학과", "category_code": "20", "year": "2026", "course_number": "011839", "name": "전공탐색", "course_type_code": "1E", "course_type_name": "공통교양필수", "credits": "1", "lecture_hours": "0", "practice_hours": "1", "total_hours": "1", "grade_level": "1", "semester_code": "2", "semester_name": "2학기", "overview": "신입생이 전공 관련 주제를 탐구하고 소논문으로 정리하는 과목 ...", "extra": { "curiYear": "2026", "studentYear": "1" } } ] ``` All fields are strings, empty when missing. | Field | School key | Meaning | |---|---|---| | `department_code`, `department_name` | `deptCd`, `deptNm` | Department code and name | | `category_code` | `orgnClsfCd` | Organization category code | | `year` | `year` | Curriculum year | | `course_number`, `name` | `curiNo`, `curiNm` | Course number, course name | | `course_type_code`, `course_type_name` | `curiTypeCd`, `curiTypeNm` | Course type code and name | | `credits` | `cdt` | Credits | | `lecture_hours`, `practice_hours`, `total_hours` | `lecTmNum`, `exeTmNum`, `curiTm` | Lecture, practice and total hours | | `grade_level` | `stuYearCd` | Suggested year level | | `semester_code`, `semester_name` | `smtDanCd`, `smtDanNm` | Suggested term code and name | | `overview` | **`remark`** | Course overview text | | `extra` | other fields | for example `curiYear`, `studentYear`, `sameSlcCnt` | - Cached for one day (86400 seconds by default) per department code and course number, with one school request per call. - School-side failures (connection, timeout, bad response, no `items`) return the last cached value or an empty list. With no cache, an empty list without a request for 60 seconds (default). - An empty `items` is a normal answer. A course that is not in that department's curriculum gives an empty list, not an error. - The only exception is an empty department code or course number (`INVALID_INPUT`). ## Known limits - Only courses in a degree department's curriculum come back. A liberal-arts college or graduate department code can give an empty list. - The overview is the curriculum text of the current curriculum year, not a text per section or instructor. - The whole curriculum list is not provided. The school's list request ignores the department code and picks the department from a screen menu number, and there is no mapping from that number to a department code. For a whole department you can call this method for each course of its lecture list, one request per course. - The instructor syllabus is not provided. Its form carries the instructor's name and contact lines. - Contract source: [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) ## Example code **Java** ```java // Look up curriculum info with a lecture's department code and course number (empty list if absent) Lecture lecture = lectures.get(0); List courses = publicData.curriculumCourses(lecture.getDepartmentCode(), lecture.getCourseNumber()); ``` **Python** ```python # Look up curriculum info with a lecture's department code and course number (empty list if absent) courses = public_data.curriculum_courses(lectures[0].department_code, lectures[0].course_number) if courses: print(courses[0].grade_level, courses[0].semester_name, courses[0].overview) ``` **Node.js** ```javascript // Look up curriculum info with a lecture's department code and course number (empty list if absent) const courses = await publicData.curriculumCourses({ departmentCode: lectures[0].departmentCode, courseNumber: lectures[0].courseNumber, }); ``` To find the course listings first, see [Course listings](https://cassiiopeia.github.io/pro-sejongpeia/en/data/lectures/). --- # Authentication methods Source: https://cassiiopeia.github.io/pro-sejongpeia/en/auth/methods/ Section: Authentication reference These are the operations that log in to the portal with a student ID and password. The three languages have the same features and the same results; only the naming differs. ## Request parameters Names follow Python; Java and Node.js use `camelCase` (`includeRaw`, `includeExtra`). Node.js takes options as an object in the last argument (`{ includeRaw: true }`). | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `student_id` | string | yes | | Student ID. If blank, `INVALID_INPUT` before any network call | | `password` | string | yes | | Portal password. If blank, `INVALID_INPUT` | | `year` | 4-digit number or string | business periods only | | Year to look up | | `month` | 1-12 or empty | no | empty (whole year) | Month to look up. Business periods only | | `include_raw` | boolean | no | `false` | If `true`, keeps the school's raw response (`raw_html`, `raw_json`) in the result. Extension: [Raw response and extra options](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/raw-and-extra/) | | `include_extra` | boolean | no | `false` | If `true`, puts the remaining unnamed SIS fields in `extra`. Extension: [Raw response and extra options](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/raw-and-extra/) | ## Method table | Operation | Java | Python | Node.js | |---|---|---|---| | Integrated: student info + classic reading + contact | `authenticate(id, pw)` | `authenticate(id, pw)` | `authenticate(id, pw)` | | DHC: student info + classic reading | `authenticateWithDHC(id, pw)` | `authenticate_dhc(id, pw)` | `authenticateDhc(id, pw)` | | SIS: student info + contact | `authenticateWithSIS(id, pw)` | `authenticate_sis(id, pw)` | `authenticateSis(id, pw)` | | SIS academic business periods (login required) | `fetchSisBusinessPeriods(id, pw, year, month)` | `fetch_sis_business_periods(id, pw, year, month="")` | `fetchSisBusinessPeriods(id, pw, { year, month })` | | Include raw response (debugging) | `authenticateWithRaw`, `authenticateWithDHCRaw`, `authenticateWithSISRaw` | `include_raw=True` | `{ includeRaw: true }` | | Include the remaining SIS fields | `authenticate(id, pw, false, true)` | `include_extra=True` | `{ includeExtra: true }` | - In Java these are methods of the `SuhSejongAuthEngine` bean that Spring Boot auto-configuration registers. For use without Spring see [Configuration](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/configuration/). - In Python the module functions (`from sejongpeia import authenticate`) use a client with default settings. Create a `SejongAuthClient` yourself to change them. - In Node.js every method returns a `Promise`. Python and Java calls are synchronous. ## Response data structure Part of an integrated result. The example uses Python `snake_case` names; Java and Node.js use `camelCase` (`studentId`, `phoneNumber`). All fields are on [Result fields](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/result-fields/). ```json { "success": true, "student_id": "20000000", "name": "홍길동", "major": "컴퓨터공학과", "email": "user@example.com", "phone_number": "010-0000-0000" } ``` | Method | Always filled | If it fails | |---|---|---| | Integrated | Student info and classic reading from DHC | If the DHC step fails, the whole call fails | | Integrated (contact etc.) | Email, phone number, English name and school codes from SIS | If the SIS step fails the call still succeeds and only those fields are `null` | | Integrated (parent unit) | `parentUnitName` from the portal main page | If the portal main step fails the call still succeeds and the field is `null` | | DHC | Student info, classic reading | Throws | | SIS | Student info, contact, school codes (`grade` and `status` are always empty strings) | Throws | | Business periods | List of periods allowed per business | Throws, without retry | - A blank student ID or password is `INVALID_INPUT`; a wrong password is `AUTHENTICATION_FAILED`. All codes are on [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/). - The integrated call logs in to the portal **once** and uses the same session for DHC, SIS and the portal main page. Calling student info and contact separately logs in twice, so prefer the one integrated call. - A wrong password is not retried: the call stops at the first failure. See [Login verdict and account lock protection](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/) for why. - Most of the time is the portal's own response time, which varies with the school server. > > Call it from the server only. Calling it directly from a browser or app exposes the password to third-party code. Never store the password or write it to logs. ## Example code **Java** ```java SejongAuthResult result = sejongAuthEngine.authenticate(studentId, password); System.out.println(result.getName() + " " + result.getMajor()); ``` **Python** ```python from sejongpeia import authenticate result = authenticate(student_id, password) print(result.name, result.major) ``` **Node.js** ```javascript import { authenticate } from 'pro-sejongpeia'; const result = await authenticate(studentId, password); console.log(result.name, result.major); ``` ## Next - [Add student verification](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/student-verification/): a club sign-up example - [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/): failure reasons and how to branch - [SSO flow](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/sso-flow/): what happens inside the integrated call --- # Result fields Source: https://cassiiopeia.github.io/pro-sejongpeia/en/auth/result-fields/ Section: Authentication reference Fields and types of what the integrated, DHC and SIS authentication return. The request side is on [Authentication methods](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/methods/); the source schema is [spec/RESULT_SCHEMA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/RESULT_SCHEMA.md). ## Response data structure Example of an integrated result. The example uses Python `snake_case` names; Java and Node.js use `camelCase` (`studentId`, `phoneNumber`). ```json { "success": true, "student_id": "20000000", "name": "홍길동", "major": "컴퓨터공학과", "grade": "3", "status": "재학", "classic_reading": { "completed_semesters": "4 학기", "certified_semesters": "4 학기", "certification_status": "인증", "certifications": [{ "area": "서양의 역사와 사상", "required_count": "1", "certified_count": "1" }], "exam_records": [], "subject_substitutions": [], "contest_records": [], "curriculum_records": [] }, "email": "user@example.com", "phone_number": "010-0000-0000", "english_name": "Gildong Hong", "user_category": "학부생", "parent_unit_name": "", "authenticated_at": "yyyy-MM-ddTHH:mm:ss", "department_code": "0000", "extra": null, "raw_html": null, "raw_json": null } ``` ### Integrated result fields | Field | Type | Source | Rule | |---|---|---|---| | `success` | boolean | | Always true when a result is returned | | `studentId` | string | DHC | Student ID | | `name` | string | DHC | Name | | `major` | string | DHC | Major | | `grade` | string | DHC | Year | | `status` | string | DHC | Enrollment status (enrolled, on leave, graduated; portal text as is) | | `classicReading` | object | DHC | See "Classic reading" below | | `email` | string or null | SIS | null if the SIS lookup fails | | `phoneNumber` | string or null | SIS | Same. Format `010-0000-0000` | | `englishName` | string or null | SIS | Same | | `userCategory` | string or null | SIS | User category, for example `학부생` | | `parentUnitName` | string or null | Portal main | null if the portal main call fails, empty string if there is no usable value | | `authenticatedAt` | date-time | | Authentication time (each language's local time type) | | 11 SIS school codes | string or null | SIS | See "SIS school codes" below. null if the SIS lookup fails | | `extra` | map or null | SIS | Only with the extra option, otherwise null | | `rawHtml` | string or null | DHC | Only with the raw option | | `rawJson` | string or null | SIS | Only with the raw option. null if SIS failed | ### When it fails - If the DHC step fails, the call throws and returns no result (`AUTHENTICATION_FAILED`, `DATA_FETCH_FAILED`, `SESSION_ERROR` and others; see [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/)). - If the SIS step fails, the call still succeeds and only the SIS fields (contact, English name, user category, school codes) are `null`. - If the portal main step fails, the call still succeeds and only `parentUnitName` is `null`. > **Do not decide anything from parentUnitName** > It is auxiliary information: the name of the unit above the major (a college or a school), extracted from the portal screen text. It can be an empty string depending on the student type, and it is portal text whose wording changes with the academic year. Never branch on a list of values and never use it for the authentication verdict. ## DHC result `success`, `studentId`, `name`, `major`, `grade`, `status`, `classicReading`, `authenticatedAt`, `rawHtml` (only with the raw option) There are no contact fields. ## SIS result `success`, `studentId`, `name`, `major`, `grade` (always an empty string), `status` (always an empty string), `email`, `phoneNumber`, `englishName`, `userCategory`, the SIS school codes, `extra` (only with the extra option), `authenticatedAt`, `rawJson` (only with the raw option) There are no classic reading fields. ## Classic reading (`classicReading`) Three portal text values from the DHC "사용자 정보" table and five sub-lists. | Field | Type | Rule | |---|---|---| | `completedSemesters` | string | `이수 학기` as is (for example `8 학기`). Empty string if missing | | `certifiedSemesters` | string | `인증완료 학기` as is. Empty string if missing | | `certificationStatus` | string | `인증여부` as is. Free text that can carry a substitution reason. Empty string if missing | | List | Item fields | |---|---| | `certifications` (status per area) | `area`, `requiredCount`, `certifiedCount` | | `examRecords` (certification exams) | `semester`, `area`, `bookTitle`, `examDate`, `score`, `passStatus` | | `subjectSubstitutions` (substitution by course) | `semester`, `subjectName`, `area`, `bookTitle`, `completion` | | `contestRecords` (contest certification) | `semester`, `contestName`, `area`, `bookTitle` | | `curriculumRecords` (course-linked certification) | `semester`, `subjectName`, `area`, `bookTitle`, `completion` | Python names are `snake_case` (`completed_semesters`, `required_count`, `book_title`, ...). A list is empty when the table has no rows. Reading the certification status per area in Java: ```java SejongDhcAuthResult result = sejongAuthEngine.authenticateWithDHC(studentId, password); for (ClassicCertification certification : result.getClassicReading().getCertifications()) { System.out.println(certification.getArea() + " " + certification.getCertifiedCount() + "/" + certification.getRequiredCount()); } ``` ## SIS school codes The school's strings as they are. The library carries no code tables or name mapping, and a missing value is an empty string. The SIS datasets are searched in the order `dm_UserInfoSch`, `dm_UserInfoGam`, `dm_UserInfo` and the first non-blank value wins. They are returned by default in the SIS and integrated results. | Field (Python) | SIS key | Note | |---|---|---| | `departmentCode` (`department_code`) | `DEPT_CD`, else `DEPT_NO` | The two hold the same department code; `DEPT_CD` is used first | | `organizationClassificationCode` | `ORGN_CLSF_CD` | | | `enrollmentStatusCode` | `STATUS_DIV_CD` | The school's raw code; no meaning mapping | | `userClassificationCode` | `USER_CLSF_CD` | | | `userClassificationLargeCode` | `USER_CLSF_L_CD` | | | `userClassificationMiddleCode` | `USER_CLSF_M_CD` | | | `roleCode` / `roleName` | `RPSN_ROLE_CD` / `RPSN_ROLE_CD_NM` | | | `menuSystemId` / `menuSystemName` | `MENU_SYS_ID` / `MENU_SYS_NM` | | | `locale` | `INTG_LOCALE` | | `departmentCode` uses the same code system as the department code in [Department and term codes](https://cassiiopeia.github.io/pro-sejongpeia/en/data/codes/). ## null versus empty string - A field of the authentication result is an empty string when the school has no value. - Fields that come from SIS or the portal main page are null only **when that lookup itself failed**. The integrated call still succeeds then. - The raw responses (`rawHtml`, `rawJson`) and `extra` are null unless the option is on. - All of these are school text, so they can change when the school changes its wording. See [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/). --- # Error codes Source: https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/ Section: Authentication reference Every language throws one error type that carries a `code`. The message is in English and may change, so always branch on the **code**. | Language | Error type | Where to read the code | |---|---|---| | Java | `SejongAuthException` | `getErrorCode()` (`SejongAuthErrorCode`) | | Python | `SejongAuthError` | `error.code` (`ErrorCode`) | | Node.js | `SejongAuthError` | `error.code` (`ErrorCode`) | ## Code table | Code | Meaning | Typical cause | |---|---|---| | `INVALID_INPUT` | Student ID or password is blank | Rejected before any network call. Also a malformed year or month for business periods | | `AUTHENTICATION_FAILED` | Wrong student ID or password | Portal verdict `erridpwd`/`Error`. As a fallback, DHC 401 or the SIS logout error | | `ACCOUNT_LOCKED` | The portal locked the account after repeated wrong passwords | Portal verdict `pwdNeedChg` | | `ACCOUNT_UNAVAILABLE` | The portal account cannot sign in | Use period ended (`invalidDt`) or blocked by the administrator (`invalid`) | | `CONNECTION_FAILED` | Cannot reach the portal | Network error, or login retries exhausted | | `CONNECTION_TIMEOUT` | Portal timed out | The last login attempt timed out | | `SESSION_ERROR` | Portal session handling error | An SSO step answered non-2xx, or an unknown login verdict value | | `DATA_FETCH_FAILED` | Failed to fetch student info | A data page answered non-200 or empty | | `PARSE_ERROR` | Response structure not as expected | The portal changed a page or JSON structure | | `SSL_CONFIGURATION_ERROR` | TLS setup failed | A TLS configuration error while creating the client | Java keeps `INVALID_CREDENTIALS`, marked `@Deprecated`. It is never thrown any more and exists only so old code still compiles; Python and Node.js do not have it. ## Account lock The Sejong portal locks an account after repeated wrong passwords. - A lock is released only by resetting the password at the [portal password page](https://portal.sejong.ac.kr/jsp/inquiry/pwinquiry.jsp). Waiting does not release it and the library cannot release it for you. - The library never retries a rejected login: it stops at the first failure without further requests. - If anyone can type a student ID into your service, **limit the failure count per student ID**. Otherwise someone can repeat failures on purpose with another student's ID and lock that student's portal account. How the verdict is read is in [Login verdict and account lock protection](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/). ## Handling examples **Java** ```java try { SejongAuthResult result = sejongAuthEngine.authenticate(studentId, password); } catch (SejongAuthException exception) { switch (exception.getErrorCode()) { case AUTHENTICATION_FAILED -> { /* wrong ID or password: raise the per-ID failure count */ } case ACCOUNT_LOCKED -> { /* point to the portal password reset */ } case ACCOUNT_UNAVAILABLE -> { /* use period ended or blocked */ } case CONNECTION_FAILED, CONNECTION_TIMEOUT -> { /* portal outage: ask to retry later */ } default -> { /* parse or session error: log the code only */ } } } ``` **Python** ```python from sejongpeia import authenticate, SejongAuthError, ErrorCode try: result = authenticate(student_id, password) except SejongAuthError as error: if error.code == ErrorCode.AUTHENTICATION_FAILED: ... # wrong ID or password: raise the per-ID failure count elif error.code == ErrorCode.ACCOUNT_LOCKED: ... # point to the portal password reset elif error.code in (ErrorCode.CONNECTION_FAILED, ErrorCode.CONNECTION_TIMEOUT): ... # portal outage: ask to retry later else: ... # parse or session error: log the code only ``` **Node.js** ```javascript import { authenticate, SejongAuthError, ErrorCode } from 'pro-sejongpeia'; try { const result = await authenticate(studentId, password); } catch (error) { if (!(error instanceof SejongAuthError)) throw error; if (error.code === ErrorCode.AUTHENTICATION_FAILED) { // wrong ID or password: raise the per-ID failure count } else if (error.code === ErrorCode.ACCOUNT_LOCKED) { // point to the portal password reset } else if (error.code === ErrorCode.CONNECTION_FAILED || error.code === ErrorCode.CONNECTION_TIMEOUT) { // portal outage: ask to retry later } else { // parse or session error: log the code only } } ``` > > Public data (departments, courses, calendar and so on) does not throw these errors when the school server fails. It returns the last cached value or an empty list. The only exception is invalid input. See [Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/). --- # Configuration Source: https://cassiiopeia.github.io/pro-sejongpeia/en/auth/configuration/ Section: Authentication reference Everything is optional. If the defaults are fine, configure nothing. ## Settings | Setting | Type | Default | Meaning | |---|---|---|---| | Request timeout | number (seconds) | 10 | Connect and read timeout. Java `sejongpeia.timeout-seconds`, Python `timeout_seconds`, Node.js `timeoutSeconds` | | Portal login retries | number | 3 | Java `sejongpeia.max-retry`, Python `max_retry`, Node.js `maxRetry` | | Certificate verification | boolean | `true` | Java `sejongpeia.ssl-verification`, Python `ssl_verification`, Node.js `sslVerification` | ## Scope of retries - Retries apply to the **portal login request** only. It is tried again on a non-2xx answer or a timeout. - A timeout on the last attempt is `CONNECTION_TIMEOUT`; running out of attempts is `CONNECTION_FAILED`. - A login the portal rejected for a wrong password or a lock is never retried, to protect the account. ## Certificate verification On by default. There is an option to turn it off, but treat it as a debugging tool: with it off, the connection that carries the login is not verified. ## Java: without Spring Spring Boot is an optional dependency, so a plain Java project does not pull it in. Wire the objects yourself. ```java import kr.suhsaechan.sejongpeia.auth.client.SejongPortalClient; import kr.suhsaechan.sejongpeia.auth.client.SejongSisClient; import kr.suhsaechan.sejongpeia.auth.model.SejongAuthResult; import kr.suhsaechan.sejongpeia.auth.parser.SejongClassicReadingParser; import kr.suhsaechan.sejongpeia.auth.parser.SejongSisParser; import kr.suhsaechan.sejongpeia.auth.parser.SejongStudentInfoParser; import kr.suhsaechan.sejongpeia.auth.service.SuhSejongAuthEngine; import kr.suhsaechan.sejongpeia.common.config.SejongAuthProperties; import kr.suhsaechan.sejongpeia.common.util.SejongTlsSupport; SejongTlsSupport.ensureLegacyCipherEnabled(); // first thing in main(), before any other HTTPS call SejongAuthProperties properties = new SejongAuthProperties(); SuhSejongAuthEngine engine = new SuhSejongAuthEngine(new SejongPortalClient(properties), new SejongStudentInfoParser(), new SejongClassicReadingParser(), new SejongSisClient(properties), new SejongSisParser()); SejongAuthResult result = engine.authenticate(studentId, password); ``` > **Java TLS caveat** > The portal offers only a cipher (`TLS_RSA_WITH_AES_256_CBC_SHA`) that current JDKs disable. The library removes `TLS_RSA_*` from `jdk.tls.disabledAlgorithms`. That setting applies to the **whole JVM** and only works **before the JVM's first TLS connection**. > > - Spring Boot handles it automatically at startup. > - Without Spring, call `SejongTlsSupport.ensureLegacyCipherEnabled()` first in `main()`. Calling it too late fails with `CONNECTION_FAILED` and a message that names the cause. > - If a JVM-wide change is not acceptable, run authentication in a separate process. > - Python and Node.js change the TLS settings of their own connections only. > > Background is in [Legacy TLS](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/legacy-tls/). ## Public data client Settings of the public school data client (`SejongPublicData`) such as cache time, failure memory time and maximum cache entries are in [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Example code **Java** ```yaml sejongpeia: timeout-seconds: 10 # request timeout (default 10) max-retry: 3 # portal login retries (default 3) ssl-verification: true # portal certificate check (default true) ``` **Python** ```python from sejongpeia import SejongAuthClient # create your own client only when you need non-default settings client = SejongAuthClient(timeout_seconds=10, max_retry=3, ssl_verification=True) result = client.authenticate(student_id, password) ``` **Node.js** ```javascript import { SejongAuthClient } from 'pro-sejongpeia'; // create your own client only when you need non-default settings const client = new SejongAuthClient({ timeoutSeconds: 10, maxRetry: 3, sslVerification: true }); const result = await client.authenticate(studentId, password); ``` --- # Raw response and extra options Source: https://cassiiopeia.github.io/pro-sejongpeia/en/auth/raw-and-extra/ Section: Authentication reference A default result has named fields only. Turn the two options on only when you need more; turning them on changes no other field. ## Request parameters Names follow Python; Java and Node.js use `camelCase`. Node.js takes them as an object in the last argument (`{ includeRaw: true }`). | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `include_raw` | boolean | no | `false` | Keeps the school's raw response in the result (`raw_html`, `raw_json`). In Java: `authenticateWithRaw`, `authenticateWithDHCRaw`, `authenticateWithSISRaw` | | `include_extra` | boolean | no | `false` | Puts the remaining unnamed SIS fields in the `extra` map. Integrated and SIS authentication only. In Java: the last argument of `authenticate(id, pw, false, true)` | ## Response data structure With an option on, the fields below are filled. The example uses Python `snake_case` names; Java and Node.js use `camelCase` (`rawHtml`, `rawJson`). ```json { "student_id": "20000000", "name": "홍길동", "extra": { "dm_UserInfoSch.FIELD_NAME": "value" }, "raw_html": "...", "raw_json": "{...}" } ``` | Field | Type | Filled when | |---|---|---| | `raw_html` | string or null | The DHC page, with the raw option. DHC and integrated results | | `raw_json` | string or null | The SIS `initUserInfo` response, with the raw option. SIS and integrated results. null if SIS failed | | `extra` | map or null | With the extra option, otherwise null. Not in a DHC result | ### Raw option Keeps the response the school sent, as is, in the result. For debugging and parser checks. - An integrated result fills both `raw_html` and `raw_json`, a DHC result `raw_html` only, a SIS result `raw_json` only. - **No extra request is made.** It only keeps responses already received. > **Never pass the raw response outward** > The SIS raw text contains values that look like a session identifier, the client IP and the login time. Do not hand it to an external client as is. ### Extra option Returns the remaining SIS fields the school provides, other than the named ones, as a `{"dataset.FIELD": "value"}` map. It is a way to reach a field the school adds without waiting for a release. - The source is the scalar fields of the datasets `dm_UserInfoSch`, `dm_UserInfoGam` and `dm_UserInfo`. ### Always-excluded keys Keys matching `extraExcludedKeyPattern` in `spec/contract.json` (case-insensitive) never enter extra. ```text (PWD|PASS|JUMIN|RRN|RESI|SSN|BANK|ACNT|ACCT|ACCOUNT|ADDR|ZIP|POST|ALIAS|SESS|TOKEN|SECRET|BIRTH|BRTH|VALID|PHONE|TEL|MOBILE|MAIL|LOGIN|RUNNING|(^|_)IP($|_)|_YN$) ``` That covers session values, client IP, password and account state flags, resident number, bank account, address, staff-name fields (`ALIAS`), phone, mail and birth date. It is still a **best-effort name pattern filter**. Do not forward extra to an external client as is; pick the keys you need. ## Outside the compatibility guarantee The keys and structure of the raw responses and the keys and values of extra change whenever the school changes them. Do not hang core app logic on them. For stable access use the named fields in [Result fields](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/result-fields/). The policy is in [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/). ## Example code ### Raw option **Java** ```java SejongAuthResult result = sejongAuthEngine.authenticateWithRaw(studentId, password); String html = result.getRawHtml(); // DHC raw String json = result.getRawJson(); // SIS raw (null if SIS failed) sejongAuthEngine.authenticateWithDHCRaw(studentId, password); // DHC result + rawHtml sejongAuthEngine.authenticateWithSISRaw(studentId, password); // SIS result + rawJson ``` **Python** ```python result = authenticate(student_id, password, include_raw=True) html, json_text = result.raw_html, result.raw_json authenticate_dhc(student_id, password, include_raw=True) # DHC result + raw_html authenticate_sis(student_id, password, include_raw=True) # SIS result + raw_json ``` **Node.js** ```javascript const result = await authenticate(studentId, password, { includeRaw: true }); const { rawHtml, rawJson } = result; await authenticateDhc(studentId, password, { includeRaw: true }); // DHC result + rawHtml await authenticateSis(studentId, password, { includeRaw: true }); // SIS result + rawJson ``` ### Extra option **Java** ```java // arguments: student ID, password, include raw, include extra SejongAuthResult result = sejongAuthEngine.authenticate(studentId, password, false, true); Map extra = result.getExtra(); ``` **Python** ```python result = authenticate(student_id, password, include_extra=True) extra = result.extra # {"dataset.FIELD": "value"} ``` **Node.js** ```javascript const result = await authenticate(studentId, password, { includeExtra: true }); const { extra } = result; // { 'dataset.FIELD': 'value' } ``` --- # Academic business periods Source: https://cassiiopeia.github.io/pro-sejongpeia/en/auth/business-periods/ Section: Authentication reference Fetches **which business is allowed in which period** (course registration, grade lookup and so on) the way the "학사일정조회" screen of the student's own student information system (SIS) does. Login is required. This is a **different data set** from the public academic calendar ([Academic calendar](https://cassiiopeia.github.io/pro-sejongpeia/en/data/calendar/)). The public calendar lists school events; this one is the per-business allowed periods that the signed-in student's own SIS provides. ## Request parameters | Parameter | Type | Required | Default | Meaning | |---|---|---|---|---| | `student_id` | string | yes | | Student ID. If blank, `INVALID_INPUT` | | `password` | string | yes | | Portal password. If blank, `INVALID_INPUT` | | `year` | 4-digit number or string | yes | | Year to look up. A malformed value is `INVALID_INPUT` without any request | | `month` | 1-12 or empty | no | empty (whole year) | Month to look up. A malformed value is `INVALID_INPUT` without any request | Method and year/month form per language: | Language | Method | Year / month | |---|---|---| | Java | `fetchSisBusinessPeriods(id, pw, year, month)` | strings (`""` month means the whole year) | | Python | `fetch_sis_business_periods(id, pw, year, month="")` | number or string | | Node.js | `fetchSisBusinessPeriods(id, pw, { year, month })` | number or string, `month` optional | ## Response data structure One item is one row of the list. The example uses Python `snake_case` names; Java and Node.js use `camelCase`. ```json [ { "business_name": "business name", "schedule_name": "schedule name", "period": "yyyy-MM-dd ∼ yyyy-MM-dd", "start_date": "yyyyMMdd", "end_date": "yyyyMMdd", "extra": {} } ] ``` Values are the school's text, and a missing value is an empty string. One odd row never fails the whole list, and an empty list is a valid answer. | Field (Python) | School key | Note | |---|---|---| | `period` | `PERIOD` | Text in the form `yyyy-MM-dd ∼ yyyy-MM-dd` | | `startDate` (`start_date`), `endDate` (`end_date`) | `START_DT`, `TO_DT` | `yyyyMMdd` as is. An open-ended period ends `29991231` | | `scheduleName` | `SCHEDULE` | Schedule name | | `courseGroupName` | `CORS_SCHE_GRP_CD_NM` | | | `target` | `TARGET_AUTH` | Target | | `businessName` | `SYS_NM` | Business name | | `yearSemester` | `YEAR_SMT` | | | `timeText` | `TMS` | | | `categoryCode` | `ORGN_CLSF_CD` | | | `sortOrder` | `PRT_ORD` | String | | `extra` | other scalar keys | Same exclusion pattern as the SIS extra ([Raw response and extra options](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/raw-and-extra/)) | ### When it fails If any step fails, it throws without retry. | Situation | Code | |---|---| | Malformed year or month, blank input | `INVALID_INPUT` | | Non-200 or empty answer, empty organization, user classification or menu code | `DATA_FETCH_FAILED` | | 401 | `AUTHENTICATION_FAILED` | | The school answers a logout error | `SESSION_ERROR` | | Any other school error | `DATA_FETCH_FAILED` | | Missing period info or a list that is not the expected structure | `PARSE_ERROR` | All codes are in [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/). ### What happens inside The school ends the session when it receives a malformed request. The library therefore sends requests only in the same order as the browser. 1. Portal login (with the SIS return address), SSO, and the SIS user info lookup 2. Menu list request: puts the menu permission into the server session 3. Open-the-screen request 4. Period list request: carries the student's own organization and user classification codes plus the year and month Everything runs in one session with one login. **If any step fails, it stops without retry.** The exact order and values are in step 2d of [spec/PORTAL_FLOW.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PORTAL_FLOW.md). > **Session values are never kept** > The SIS session values (integrated user number, run values, login time) never appear in a result, a log line or an error message. The school's own error text is not copied into the message either; only the step name is. If the school changes the screen, values or structure can change. ## Example code **Java** ```java // one month (an empty month "" returns the whole year) List periods = sejongAuthEngine.fetchSisBusinessPeriods(studentId, password, "2026", "10"); for (SisBusinessPeriod period : periods) { System.out.println(period.businessName() + " " + period.scheduleName() + " " + period.period()); } ``` **Python** ```python from sejongpeia import fetch_sis_business_periods # one month (an empty month returns the whole year) periods = fetch_sis_business_periods(student_id, password, 2026, 10) for period in periods: print(period.business_name, period.schedule_name, period.period) ``` **Node.js** ```javascript import { fetchSisBusinessPeriods } from 'pro-sejongpeia'; // one month (omit month to get the whole year) const periods = await fetchSisBusinessPeriods(studentId, password, { year: 2026, month: 10 }); for (const period of periods) console.log(period.businessName, period.scheduleName, period.period); ``` --- # Legacy TLS Source: https://cassiiopeia.github.io/pro-sejongpeia/en/understand/legacy-tls/ Section: Understand ## Why it is hard The Sejong portal offers **only one** cipher on TLS 1.2: `TLS_RSA_WITH_AES_256_CBC_SHA` (OpenSSL name `AES256-SHA`). It is an old construction, and current JDK and OpenSSL defaults switch it off. An HTTPS client with default settings therefore fails at the handshake and never reaches the login form. ## What the library does Each language solves it in its own way. | Language | Method | Scope | |---|---|---| | Python | Set the cipher list of the `ssl` context to `DEFAULT:AES256-SHA:@SECLEVEL=1` | This library's connections only | | Node.js | `https.Agent({ ciphers: 'DEFAULT:AES256-SHA:@SECLEVEL=1' })` | This library's connections only | | Java | Remove `TLS_RSA_*` from `jdk.tls.disabledAlgorithms` | **The whole JVM** | - With OpenSSL 3.x, Python and Node.js must add this cipher to the list explicitly and lower the security level to `1` to use it. - `@SECLEVEL=1` is kept on purpose: level 0 would also accept weak certificate keys. - Certificate verification stays on by default ([Configuration](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/configuration/)). ## The Java catch JSSE reads this security property **once, at the JVM's first TLS use**. Changing it later has no effect. - Spring Boot: `SejongTlsEnvironmentPostProcessor` handles it automatically at startup. - Plain Java: call `SejongTlsSupport.ensureLegacyCipherEnabled()` first in `main()`. If another HTTPS call happens first, it is too late. - Calling it too late fails with `CONNECTION_FAILED`, and the message names the cause and the fix. - The change applies to the whole JVM, so other code in the same process may use this cipher too. If that is not acceptable, run authentication in a separate process. ```java public static void main(String[] args) { SejongTlsSupport.ensureLegacyCipherEnabled(); // before any other HTTPS call // ... start the app } ``` > **If you implement it yourself** > The default TLS settings of a language will not connect to the portal. When you get a connection error, first check whether the cipher the server offers overlaps with what your runtime allows. How far you lower the setting (one connection or the whole process) has different security consequences, so choose it deliberately. ## Next - Source table: Transport requirements in [spec/PORTAL_FLOW.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PORTAL_FLOW.md) - The next barrier: [Firewall and User-Agent](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/firewall-user-agent/) --- # Firewall and User-Agent Source: https://cassiiopeia.github.io/pro-sejongpeia/en/understand/firewall-user-agent/ Section: Understand ## Why it is hard Sometimes login works and TLS passes, yet requests fail oddly. The cause is the `User-Agent` request header. Two behaviors were found in the portal flow. | Situation | Result | |---|---| | No `User-Agent` sent | `classic.sejong.ac.kr` answers **404** after SSO | | A library default (for example `Python-urllib`) | The firewall of `sjpt.sejong.ac.kr` blocks it and returns an "Alert!!!" HTML page | Your code is correct in both cases and only the response is strange, so it is easy to blame parsing or the login. ## What the library does It sends a non-empty, non-default, identifying `User-Agent` on every request. The format fixed in the contract is: ```text pro-sejongpeia-/ ``` - Python: `pro-sejongpeia-python/` - Node.js: `pro-sejongpeia-node/` - Java public data calls: `pro-sejongpeia-java` - Authentication requests follow the format above. ## If you implement it yourself - Set a non-empty, distinctive `User-Agent` on every HTTP client. - Rather than imitating a browser, leave the library name and version visible. It makes finding the cause easier for both sides when something goes wrong. - When you get a 404 or an "Alert!!!" page, suspect the header before the parser. ## Next - [Legacy TLS](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/legacy-tls/) - [SSO flow](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/sso-flow/) - Source: [spec/PORTAL_FLOW.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PORTAL_FLOW.md) --- # SSO flow Source: https://cassiiopeia.github.io/pro-sejongpeia/en/understand/sso-flow/ Section: Understand ## Why it is hard Sejong services are split across domains (`portal`, `classic`, `sjpt`). The student ID and password go to the portal only; the rest are entered with the SSO cookie the portal sets. A browser does this on its own, but server code has to do it explicitly. - The SSO cookie (`ssotoken`) is set for all of `.sejong.ac.kr`, so you need a cookie jar that honors `Domain=.sejong.ac.kr`. - The SSO step answers with redirects such as `303`. Follow 301, 302, 303 and 307 automatically. - Create a new cookie jar per call, or sessions mix between users. ## Request order of the integrated call There is one login, and the same cookie jar reads three places. 1. **Portal login.** Send the student ID and password as a form to the login address of `portal.sejong.ac.kr`. Read the verdict inside the returned 200 response and continue on success ([Login verdict](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/)). 2. **DHC (`classic.sejong.ac.kr`).** Open the SSO return address to create the session, fetch the classic reading status page and parse its HTML tables. Student info and classic reading come from here. A 401 is `AUTHENTICATION_FAILED`, any other bad answer is `DATA_FETCH_FAILED`, and an SSO step error is `SESSION_ERROR`. 3. **SIS (`sjpt.sejong.ac.kr`).** Open the SSO address, call the user info API and parse the JSON. Email, phone number, English name and school codes come from here. A failure in this step does not fail the integrated call. 4. **Portal main (`portal.sejong.ac.kr`).** Extract the name of the unit above the major from the main page. It is tried once, and on failure `parentUnitName` is null. | Step | Host | Format | If it fails | |---|---|---|---| | 1 Login | `portal.sejong.ac.kr` | Form POST, verdict inside a 200 | The whole call fails | | 2 DHC | `classic.sejong.ac.kr` | HTML | The whole call fails | | 3 SIS | `sjpt.sejong.ac.kr` | JSON | Only the contact fields are null | | 4 Portal main | `portal.sejong.ac.kr` | HTML | Only `parentUnitName` is null | SIS SSO also works after a DHC login, so one login can serve both DHC and SIS. ## When you call DHC or SIS only - DHC authentication runs step 2 after the login. SIS authentication logs in with the SIS return address and runs step 3. - The business periods lookup logs in with the SIS return address; after the SIS user info it continues with the menu list, the open-the-screen and the list requests ([Academic business periods](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/business-periods/)). > **If you implement it yourself** > Separate the cookie jar per call, follow redirects, and tell the response codes of each step apart when you turn them into errors. The detailed requests and headers are in [spec/PORTAL_FLOW.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PORTAL_FLOW.md); per-screen descriptions are in the [DHC login flow](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/docs/DHC_LOGIN_FLOW.en.md) and the [SIS login flow](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/docs/SIS_LOGIN_FLOW.en.md). ## Next - [Login verdict and account lock protection](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/) - [Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/) --- # Login verdict and account lock protection Source: https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/ Section: Understand ## Why it is hard The portal answers a failed login with **HTTP 200** too, so the status code cannot tell success from failure. The result is inside a script of the small HTML page that comes back. ```text var result = '' ``` On top of that, every wrong password raises the portal's failure count, and past a certain count the account is locked. Retrying failed logins out of habit locks the user's account. ## The verdict flow at a glance > Diagram: After the portal login the library reads the verdict inside the response. A wrong password or a locked account stops at the first failure with an error code, and success queries DHC and SIS in the same session ## Verdicts and error codes | `result` value | Meaning of the portal message | Library | |---|---|---| | `OK` | Success (redirects to the return address) | Continue | | `erridpwd`, `Error` | ID or password does not match | `AUTHENTICATION_FAILED`, stop with no further requests | | `pwdNeedChg` | Account locked after repeated wrong passwords | `ACCOUNT_LOCKED` | | `invalidDt`, `invalid` | Use period ended, or blocked by the administrator | `ACCOUNT_UNAVAILABLE` | | Any other value | Error while processing the login | `SESSION_ERROR` | | No `var result` | Treated as a changed portal page format | Continue; a 401 in a later step decides | `spec/contract.json` is the single source of this mapping, and the tests of all three languages check they use the same values. ## The library's rules - A login the portal rejected (wrong password, lock, unavailable) is **never retried.** It stops at the first failure. - Retries are for **transport problems** only, such as non-2xx answers or timeouts ([Configuration](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/configuration/)). - The integrated call logs in once, so the failure count rises at most once per call. - Blank input ends with `INVALID_INPUT` without sending any request. ## What was not verified about the lock count Comments in the portal source say the lock happens after 5 failures, and the portal's own notice says about 5. But **the exact count, and whether a successful login resets it, are not verified.** Treat it conservatively. > **Limit per student ID in your service** > If anyone can type a student ID into your form, limit the failure count per student ID on your own server. The library can only stop retries inside one call; it cannot stop repeated requests. A locked account is released only when the user [resets the password on the portal](https://portal.sejong.ac.kr/jsp/inquiry/pwinquiry.jsp). ## If you implement it yourself - Read the verdict in the response body, not the status code. Samples are `spec/fixtures/portal_login_ok.html` and `portal_login_erridpwd.html` in the repository. - Treat an unknown verdict as an error, not as success. - Do not resend a rejected login automatically. ## Next - [Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/) - [SSO flow](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/sso-flow/) - Source: Step 1 of [spec/PORTAL_FLOW.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PORTAL_FLOW.md) --- # Failure isolation and cache Source: https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/ Section: Understand ## Why it is needed School servers can be slow (the extracurricular schedule sometimes takes tens of seconds), can stop answering for a while, or can change the shape of a response. If the whole app stopped whenever one place failed, that would be a problem. And sending the same request to the school once per user would put load on its servers. ## The flow of one call > Diagram: A public data call returns the cache if it is under 24 hours old. Otherwise it sends one request to the school; on success it stores and returns the result, on failure it returns the last cached value or an empty list ## Failure isolation of public data Every method of the public data client (`SejongPublicData`) follows the same rules. - A connection failure, timeout, non-2xx status, non-JSON body, missing expected key or a school `_SUBMIT_ERROR_` answer all return **without an exception**: the last cached value (even if expired) or an empty value (an empty list, an empty object for dormitory notices). - Internal school server details (such as `STACK_TRACE`) are never logged or returned. - Only **invalid input** throws: `curriculum_courses` with a blank department code or course number, and `graduate_calendar` with a malformed site id (`INVALID_INPUT`, no request). `lectures` never throws for its filters. - A missing value is an empty string (`0` for `views` and `total`, `0.0` for coordinates), and one odd row never fails the whole list. Only a missing or non-array container (`data`, `items`, `events` and so on) counts as a failure. > **Two meanings of an empty list** > In public data an empty list cannot tell "there really is no data" from "the school had a problem and nothing could be fetched". Show a message such as "could not load" next to it. If you need to, judge the meaning yourself on the calling side. ## Cache | Item | Default | Behavior | |---|---|---| | Cache lifetime | 86400 s (24 hours) | Stored in memory per method and arguments. A fresh entry means no request | | Failure memory | 60 s | After a failure with no cached value, the same call returns the empty value without a request for this long. A stale cached value is still returned on failure | | Maximum entries | 500 | When full, the oldest entry is evicted | - Concurrent calls with the same arguments share **one** request. - Locks are per key but released together with their entries, so even user-typed search text does not make memory grow without bound. - One call is one request as a rule. The exception is `cafeteria_places`: it requests ids 1 to 10 **one after the other**, up to 10 requests, and one failed request fails the whole list so that a partial list is never cached for a day. - A whole undergraduate term of lectures is a few MB, so one cache entry is large. Prefer one department at a time ([Course listings](https://cassiiopeia.github.io/pro-sejongpeia/en/data/lectures/)). - Create **one** `SejongPublicData` per process. The cache and the request merging live inside the instance. The settings (`cache_ttl_seconds`, `failure_cache_seconds`, `cache_max_entries`) are in [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). ## Failure isolation in authentication | Step | If it fails | |---|---| | Input check, portal login | The whole call fails | | DHC (student info, classic reading) | The whole call fails | | SIS (contact, school codes) | The call succeeds, only those fields are null | | Portal main (`parentUnitName`) | The call succeeds, only that field is null | Authentication and public data never affect each other's failures. A dependency rule test keeps the two code bases from importing each other. ## If you implement it yourself - Do not let an external server failure become an app failure, and keep the last good value. - Remember a failure briefly instead of repeating the same request right away. - Merge concurrent requests. ## Next - Source: Failure isolation and cache in [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md) - [Daily live portal check](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/daily-live-check/) --- # Daily live portal check Source: https://cassiiopeia.github.io/pro-sejongpeia/en/understand/daily-live-check/ Section: Understand The portal changes without notice. So that you do not learn about it from user complaints, the `PROJECT-SEJONG-LIVE-CHECK` workflow in the repository runs two checks every day. Below is only what the workflow and its scripts **actually do**. ## Schedule - Runs every day at 09:00 KST (cron `0 0 * * *`, in UTC). It can also be started manually. - The two checks are separate jobs and do not affect each other's result. ## 1. Live login check `scripts/live-check.sh` does the following. 1. Java, Python and Node.js each run one integrated authentication with the **correct password** and summarize every field as a hash. 2. The three summaries are compared. All three languages must succeed, the result must have fields, **no field may be null** (this catches a SIS lookup that failed quietly), and the hash of every field must be the same in the three languages. 3. The output is field names with a `same` / `DIFFERENT` / `NULL` status only. No personal values are printed. There are account lock safeguards. - **No wrong password is ever tried.** Each language logs in to the portal once. - While a check-failure issue is open, the scheduled run skips the login. Confirm the fix with a manual run. - If the test account is not configured in the repository, this check is skipped. On failure, one issue is opened automatically (none is created if one is already open). ## 2. Shape check without login No account is needed, so there is no lock risk, and it runs every day regardless of the issue state of the login check above. - `scripts/probe-reference-data.py`: sends 3 requests (1 second apart) to the department and term code APIs and checks that the dataset keys and required fields are unchanged. - `scripts/probe-public-data.py`: checks the response shape of public data sources such as course listings, department curriculum, academic calendar, dormitory menu and notices, cafeterias, the campus map and extracurricular programs. - The output is counts and names of missing fields only. - A network error or timeout is printed as `SKIP` and is not a failure. The library isolates failures, so the only thing the check wants to catch is **a changed shape**. - When a changed shape is detected, one issue is opened (none if an issue with the same title is already open). ## What it does not guarantee - There is one test account, so behavior for other student types (graduate students and others) is not covered by this check. - The shape check looks at whether fields exist, not whether the values are right. - It runs once a day, so a change in between is unknown until the next check. There is no promised fix time (SLA) ([API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/)). > > The test account details are used only as repository secrets and are not written here. To run the live portal tests yourself, see [Issues and pull requests](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/issues-and-prs/). ## Next - [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/): what happens when the school changes something - [Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/) --- # Add a new data source Source: https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/add-data-source/ Section: Contribute How to add new data the school serves publicly (course lists, menus, other public data). Sources are independent and share only the common layer (`common`: errors, HTTP transport, version). Authentication (`auth`) is one source just like public data, so you can build yours without touching the others. ## Procedure 1. **Propose it in an issue.** Use the `새 정보 출처 제안` (new data source proposal) issue template. Describe where it is served, whether login is needed, the request form (JSON or HTML), the fields, and whether it contains personal data. Information that exists only as HTML is not used as a primary source. 2. **Write the contract first.** Put the address, request keys and fixed values under `publicData` in `spec/contract.json` (each language keeps its own constants and its contract test compares them), then add the method, request and field mapping to `spec/PUBLIC_DATA.md`, one line each, with the date checked and the account type you used. Add the same method to the agent-facing method list `spec/api-index.json` too (parameters, return model, Java signatures). `python3 scripts/check-api-index.py` fails if it drifts from the three implementations. 3. **Create synthetic fixtures and expected results.** Add them to `FIXTURES` and the per-source `map_*()` functions (merged by `map_all()`) in `scripts/generate-public-data-fixtures.py` and run it. Never copy a real response, and never put in values that identify a person, such as a staff name. 4. **Implement Java, Python and Node in the same PR.** Put the request building and mapping in one file per source (`_source.py`, `Source.ts`, `Source.java`) and add only a one-line public method to `SejongPublicData`. Reuse the shared file (`fetcher`) for cache and failure isolation, and follow the contract's value rules (empty string when missing, `extra`). 5. **Write only the tests you need.** One test that the fixture result equals the expected result is enough. Do not add tests that call the school's servers. 6. **Add a shape check.** Add a row to `SOURCES` in `scripts/probe-public-data.py`. It runs without an account. 7. Go through the [feature exposure checklist](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/structure/). ## Where the code goes For a public-data source. One method maps to three files. | Language | Method and request | Result model | Test | |---|---|---|---| | Python | `python/src/sejongpeia/public_data/_source.py` (request, mapping), the public method line in `client.py` | `public_data/models.py` | `python/tests/test_public_data.py` | | Node | `node/src/publicData/Source.ts` (request, mapping), the public method line in `client.ts`, argument objects in `queries.ts` | `publicData/models.ts` | `node/test/publicData.test.mjs` | | Java | `publicdata/Source.java` (request, mapping), the public methods in `SejongPublicData.java` | `publicdata/model/` | `SejongPublicDataTest.java` | - Method names are `snake_case` in Python and `camelCase` in Node and Java. School fields you do not map go into `extra`. - Login-required sources are not covered by this procedure. Sources cannot import each other, so how a login session would be shared needs a design; agree on the direction in an issue first. - When you add or change a public method or result field, check that the "Public API" list in [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/) and the [integration examples](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/student-verification/) are still correct. ## Dependency rule > Diagram: The entry point assembles authentication and public data, and both import only the common layer. Authentication and public data never import each other The rule has three layers. Common (`common`) imports no source, and each source (`auth`, `public_data`) imports only common and never another source. The top-level entry points (`__init__.py`, `index.ts`, Spring `config`) only assemble, so they may import every layer. Tests enforce it. | Language | Rule test | |---|---| | Python | `python/tests/test_dependency_rules.py` | | Node | `node/test/dependencyRules.test.mjs` | | Java | `DependencyRulesTest` | Inside public data, source files (`*_source`) never import each other or the entry point (`client`); they use only the shared files (`fetcher`, `row_reader`, `models`). If a rule fails, move what needs sharing into `common`. **Folder layout** (same shape in all three languages) | Layer | Python `python/src/sejongpeia/` | Node `node/src/` | Java `java/src/main/java/kr/suhsaechan/sejongpeia/` | |---|---|---|---| | Common | `common/` (`errors`, `transport`, `version`) | `common/` (`errors`, `transport`, `version`) | `common/` (`exception`, `util`, `http`, `config`) | | Authentication | `auth/` (`client`, `parsers`, `portal_main_parser`, `models`) | `auth/` (`client`, `parsers`, `portalMainParser`, `models`) | `auth/` (`client`, `parser`, `model`, `service`) | | Public data | `public_data/` (`client`, `fetcher`, `row_reader`, `models`, `*_source`) | `publicData/` (`client`, `fetcher`, `rowReader`, `queries`, `models`, `*Source`) | `publicdata/` (`SejongPublicData`, `*Source`, `PublicDataFetcher`, `model`) | | Assembly | `__init__.py` | `index.ts` | `config/` (Spring auto-configuration, TLS setup) | ## What goes in spec/ The contract between the school's services and the three implementations lives in `spec/`. When the school changes something, change `spec/` first, then all three implementations in the same PR. | Kind | Location | Rule | |---|---|---| | Request and response contract, field meanings, limits | `spec/.md` | One document per source. Write only what was verified, and say what was not | | Values shared by the implementations (URLs, error messages) | `spec/contract.json` | The single source. Each language's contract test fails if its constants drift | | Where the school serves what, dead ends, what was not checked | `spec/REFERENCE_DATA_SURVEY.md` | Read it before searching again. Update it when you look somewhere new | | Sample responses for tests | `spec/fixtures/` | Synthetic only. Never real responses, personal data, session values or staff names | | Shape drift detection | `scripts/probe-*.py` | Runs without an account, one entry per source | ## Rules that keep it safe - Return the school's raw strings. No value lists or code tables in the library, no guessing a code from a name. - A failure in one source never blocks another, authentication in particular. Return an empty value or the last cached value. - Do not log or return internal server details (stack traces, session identifiers, IP addresses, password-state flags). - Be gentle with public pages and endpoints: cache, space requests out, and bound the number of requests per refresh. See also [Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/) and [Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/). --- # Repository structure Source: https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/structure/ Section: Contribute ## Folders | Folder | What | |---|---| | `java/` | Java / Spring Boot library (`kr.suhsaechan:sejongpeia`) | | `python/` | Python package (`pro-sejongpeia`, import `sejongpeia`) | | `node/` | Node.js package (`pro-sejongpeia`, TypeScript) | | `spec/` | Shared contract: portal flow, result schema, sanitized response samples used by every language's tests | | `website/` | This documentation site (Astro Starlight). Korean is the default, English is under `/en/` | | `scripts/` | Scripts for live portal checks, shape checks, fixture generation and docs checks | | `docs/` | Integration examples, API stability, login flow documents and images | All three languages share one version number. `version.yml` holds it and the release workflow updates it. The folder layout inside each language (`common`, `auth`, `public_data`) is in the table on [Add a new data source](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/add-data-source/). ## Layers and dependency direction The common layer calls no source, and authentication and public data never call each other. Tests enforce the rule ([Add a new data source](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/add-data-source/)). > Diagram: The entry point assembles authentication and public data, and both import only the common layer. Authentication and public data never import each other ## Development setup ```bash ./gradlew build # Java (JDK 17+, Gradle Wrapper included) cd python && python -m pip install pytest && python -m pytest # Python 3.9+ cd node && npm ci && npm test # Node 18+ ``` ## Tests against the real portal Live tests log in to the real Sejong portal, so they only run when you provide an account. - Python and Node: set `SEJONG_TEST_STUDENT_ID` and `SEJONG_TEST_PASSWORD`, then run the tests as above. - Java: create `java/src/test/resources/application-dev.yml` (in `.gitignore`). ```yaml sejong: auth: test: student-id: "your student ID" password: "your password" ``` The `실제인증_*` tests in `SuhSejongAuthEngineTest` are `@Disabled`. Remove it locally and restore it before committing. - All three at once, comparing results field by field (no personal data printed): ```bash SEJONG_TEST_STUDENT_ID=... SEJONG_TEST_PASSWORD=... scripts/verify-live.sh ``` - Correct password only (no wrong-password attempts): `scripts/live-check.sh`. The `PROJECT-SEJONG-LIVE-CHECK` workflow runs it daily and opens an issue when it fails. See [Daily live portal check](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/daily-live-check/). :::caution Several wrong passwords in a row can lock the portal account. If a run fails, find the cause before running it again. ::: ## When the portal changes 1. Update `spec/PORTAL_FLOW.md` and `spec/contract.json`, and `spec/fixtures/` if the response shape changed (with personal data removed). Each language's contract test fails until its constants match `spec/contract.json`. 2. Fix Java, Python and Node in the **same PR**. 3. Run each language's tests and `scripts/verify-live.sh`. ## Documentation languages - The default document is Korean `X.md`; translations are `X..md` (for example `README.en.md`). Add a new language the same way. - The language line at the top of each document links every language of that document. - When you change a Korean document, update its translations in the same PR. If you cannot, say so in the PR description. - The `## 최신 버전 :` line in `README.md` is updated by the release workflow; do not edit it by hand. - Check with `python3 scripts/check-docs.py` (broken relative links, missing language links). `PROJECT-SEJONG-DOCS-CHECK` also runs on pull requests. - Every page of this site (`website/`) has a Korean and an English version. Change both in the same PR. ## Feature exposure checklist Building a feature is not the end: it has to show up everywhere people look. Check this list before opening the PR. The PR template has the same checklist. - README in Korean and English: the feature and a usage example, and the `Result fields` table if fields were added - `python/README.md`, `node/README.md`: the PyPI and npm package pages - `spec/`: the contract document, `spec/contract.json`, synthetic fixtures - The documentation site `website/`: feature intro, field table, the matching reference page (Korean and English) - Package descriptions and keywords: `python/pyproject.toml`, `node/package.json`, `java/build.gradle` - A one-line summary for the release notes (the commit message) - Only promote what is actually supported Next, read the [issue and PR](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/issues-and-prs/) rules. --- # Issues and pull requests Source: https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/issues-and-prs/ Section: Contribute Bug reports, fixes for portal changes and docs improvements are all welcome. Issues and PRs in English or Korean are fine. ## Start with an issue - Bugs: include the error code (`SejongAuthException.getErrorCode()` and so on), the method you called, and the language and version. - If authentication broke because the portal changed, first check whether the [live demo](https://cassiiopeia.github.io/pro-sejongpeia/en/#demo) reproduces it. - For larger changes, agree on the direction in an issue before opening a PR. - For a new school data source, use the `새 정보 출처 제안` (new data source proposal) template. The steps are in [Add a new data source](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/add-data-source/). :::danger Never post student IDs, passwords, names, contact details, session values or raw real responses in an issue, PR or comment. Use field names and made-up values only. ::: ## Labels | Label | Use | |---|---| | `good first issue` | Fits someone who is new | | `help wanted` | Issues that need a hand | | `data-source` | Proposals of new school data sources | ## Where to post | You want to | Go to | |---|---| | Ask a usage question, suggest something, share a data source idea | [Discussions](https://github.com/Cassiiopeia/pro-sejongpeia/discussions) | | Report a bug (a reproducible problem) | [Issues](https://github.com/Cassiiopeia/pro-sejongpeia/issues/new/choose) | | Report a vulnerability | [Private reporting](https://github.com/Cassiiopeia/pro-sejongpeia/security/advisories/new), not a public issue | | Fix or add something yourself | [CONTRIBUTING.en.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/CONTRIBUTING.en.md) | Check the [FAQ](https://cassiiopeia.github.io/pro-sejongpeia/en/faq/) and the [error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/) first. This is a personal open-source project and answers come from volunteers, so they can take time. ## Security reports This library handles Sejong University portal passwords, so security reports are taken seriously. **Do not open a public issue.** 1. Go to the repository's [Security tab](https://github.com/Cassiiopeia/pro-sejongpeia/security). 2. Click **Report a vulnerability**. 3. Describe the issue, the affected version and the steps to reproduce. Never include real student IDs, passwords or other personal data. You can expect a first response within 7 days. Only the latest released version receives security fixes. The scope is this library's code, for example leaking passwords or personal data through logs, exceptions or results, unsafe TLS handling, or session mix-ups between calls. Details are in [SECURITY.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/SECURITY.md). ## Pull request rules - Branch from `main`. - Add tests when behavior changes. Parser changes need a fixture in `spec/fixtures/` and the same expected values for all languages. - When the portal changed, fix the three languages in the same PR. The steps are in [Repository structure](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/structure/). - Make sure no password or personal data ends up in logs or exception messages. - For a new feature, go through the feature exposure checklist. ## PRs written with an AI agent PRs written with a coding agent are welcome. The person submitting the PR must run the commands in the repository's [AGENTS.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/AGENTS.md) and confirm they pass, and the PR description should say which agent was used. Never include personal data or real school responses (synthetic values only). The review bar is the same as for human-written PRs. ## Code of conduct and license Participants follow the [Code of Conduct](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/CODE_OF_CONDUCT.en.md). Contributions are released under the [MIT License](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/LICENSE). --- # API stability Source: https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/ Section: More An honest description, for developers who depend on this library, of what is public API, what the version number means, and what may change. > **No backward compatibility guarantee** > Breaking changes can happen even within `1.x`. Pin the exact version. ## The short answer | Question | Answer | |---|---| | Do you guarantee backward compatibility? | **No.** The project does not aim for backward compatibility (a maintainer decision). Breaking changes can happen even within `1.x`. | | Then what does the version number mean? | Release order. Whether a release breaks something is told by the **release notes**, not by the number. | | How do I use it safely? | **Pin the exact version**, read the release notes before upgrading, and follow "Using it safely" below. | | What if the school changes something? | A daily check runs against the live school servers; when something breaks an issue is opened and fixed quickly. There is no time commitment (SLA). | | Are the three languages the same? | Features, error codes, result fields and value rules are kept identical and tests enforce it. Naming style and a few language-specific features differ. | ## Public API and internals "Public API" is what gets a release-note line and a docs update (README, `spec/`) when it changes. "Internal" changes without notice. | Language | Public API | Internal (do not use directly) | |---|---|---| | Python | The names in `__all__` of the `sejongpeia` package: `authenticate`, `authenticate_dhc`, `authenticate_sis`, `fetch_sis_business_periods`, `SejongAuthClient`, `SejongPublicData`, the result and model classes, `SejongAuthError`, `ErrorCode` | the modules and helpers in `sejongpeia.common.*`, `sejongpeia.auth.*`, `sejongpeia.public_data.*` (only the names imported with `from sejongpeia import ...` are public); names and arguments starting with an underscore (`_session_factory`, `_clock`) | | Node | The functions, classes and types the package entry point (`pro-sejongpeia`) exports. The `exports` field of `package.json` opens only this one entry point | everything the entry point does not export; options starting with an underscore (`_sessionFactory`, `_nowMilliseconds`) | | Java | The public methods of `auth.service.SuhSejongAuthEngine`, `auth.model.*`, `common.exception.*`, `publicdata.SejongPublicData` and `publicdata.model.*`, `common.util.SejongTlsSupport`, `common.config.SejongAuthProperties`, the property prefix `sejongpeia.*` | `common.util.CommonUtil`, the caller, mapper and fetcher classes in `publicdata` | Java's `auth.client.*` and `auth.parser.*` are public because using the library without Spring means building and wiring them yourself. Their **constructor shapes can still change** (semi-public). With Spring Boot auto-configuration you are not affected. Even inside the public API, the following is **outside the guarantee**. - `rawHtml` and `rawJson` returned by the raw option (`include_raw`, `includeRaw`, `authenticateWithRaw`): school text whose keys and structure follow the school. See [Raw response and extra options](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/raw-and-extra/). - Keys and values of `extra`: they change when the school adds or removes a field. `extra` is only a way to reach fields the library has no name for. - Error message wording: always branch on the error **code**. ([Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/)) - Log wording, request order, internal cache implementation. The cache defaults are configurable and documented, but the implementation may change. ## Version rule - The current line is `1.x`. The three languages (Java, Python, Node) share **one version number**. - The release workflow **bumps the patch number (third digit) automatically.** Minor and major are bumped by the maintainer by hand. - The number does **not** follow the [SemVer](https://semver.org/) promise (patch is compatible, only major breaks). Behavior-changing changes can ship in an automatic patch release. - Being at `1` does not mean a stability declaration. - How breaking changes are announced: every release gets a one-line summary on [GitHub Releases](https://github.com/Cassiiopeia/pro-sejongpeia/releases) and in [CHANGELOG.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/CHANGELOG.md), and a changed name or signature is stated in that line. Renames and signature changes (method or field names, package paths, configuration keys) can land at once, with no compatibility alias. Expect that kind of change to be possible again. ### What we still do Compatibility is not promised, but we try not to surprise you. - New features prefer **adding** (result fields, methods) over changing existing fields. - When a name or shape must change, the three languages and the docs change **in one PR**. - Changes that follow the school aim to keep your code working, but we cannot prevent the school data itself from changing. ## What the three languages keep identical These must be the same in every language, and tests fail when they drift. | Kept identical | How it is enforced | |---|---| | Request URLs, portal verdict to error code mapping and other shared values | [spec/contract.json](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/contract.json) is the single source; each language's contract test compares its constants | | The 10 error codes (`AUTHENTICATION_FAILED`, `ACCOUNT_LOCKED`, ...) and their meaning | [spec/RESULT_SCHEMA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/RESULT_SCHEMA.md), contract tests | | Authentication result fields and meaning (only the spelling differs) | RESULT_SCHEMA, expected-value tests on the sanitized sample responses in `spec/fixtures/` | | The 15 public-data methods, their result fields and value rules (missing value is an empty string, failure isolation, cache) | [spec/PUBLIC_DATA.md](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/spec/PUBLIC_DATA.md), one shared expected-result file (`expected_public_data.json`) compared in all three languages | | Sources (authentication, public data) import only the common layer and never each other | dependency-rule tests (Python, Node, Java each) | | Results against the real portal | a daily live login check in all three languages, field-by-field hash comparison (`scripts/verify-live.sh`) | **What differs by language** is intentional. - Naming: Python `snake_case`, Node and Java `camelCase`. Java names a few methods differently, as in `authenticate_dhc` / `authenticateDhc` / `authenticateWithDHC`. The mapping is in [Authentication methods](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/methods/). - Call style: Python and Java are synchronous, Node returns a `Promise`. - Language-specific: Spring Boot auto-configuration and the `sejongpeia.*` settings (Java), `SejongTlsSupport` (Java), the old code `INVALID_CREDENTIALS` (kept only in Java, never thrown). - Date-time type: the authentication time uses each language's own type. ## Deprecation - A name we want to remove is kept for a while where possible, written up in the docs and release notes, and marked `@Deprecated` in Java. Example: `SejongAuthErrorCode.INVALID_CREDENTIALS` is no longer thrown but remains so existing code compiles. - **No period is promised.** Something can disappear or change without a deprecation marker. The release notes (see "Version rule") are the only official signal. ## School data is the school's own text School fields in public data and authentication results are **the strings the school returned, as is.** The library carries no code tables or name mappings. - Values such as department codes, organization names, cafeteria numbers, schedule titles and enrollment status labels **change when the school changes them.** Do not write code that branches on a list of values. For example `parentUnitName` is a label that changes each academic year and must not be used to decide authentication. - Date formats also differ per school source: the academic calendar uses `yyyy-MM-dd`, the graduate calendar `MM.dd`. Each format is written on the [Academic calendar](https://cassiiopeia.github.io/pro-sejongpeia/en/data/calendar/) page. - A missing value is an empty string. Public data never has `null`; the SIS fields of authentication are `null` when that lookup failed. - School fields the library does not map land in `extra`; when the school adds a field it can show up in `extra` without a new release. ### When the school changes something 1. A check runs every day at 09:00 KST. The three languages really log in with a correct password (no wrong-password attempts), and the response shapes of the public data sources that need no account are verified. ([Daily live portal check](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/daily-live-check/)) 2. When it detects a problem an issue is opened automatically. 3. The contract documents (`spec/`) and the three implementations are fixed in one PR and released. Public data does not throw when one source fails: it returns the last cached value or an empty list. So **an empty list cannot tell "no data" from "a problem on the school side".** Decide what it means on the calling side if that matters. ([Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/)) ## Using it safely - **Pin the exact version** of the dependency. For example `pro-sejongpeia==`, `"pro-sejongpeia": ""`, `kr.suhsaechan:sejongpeia:`, where `` is the latest release number. A range (`^`, `~=`, `+`) takes breaking changes automatically. - Read the [release notes](https://github.com/Cassiiopeia/pro-sejongpeia/releases) before upgrading and run your tests first. - Wrap the library in one thin adapter inside your app, so there is one place to fix when something changes. - Branch on error **codes**, not messages. - Do not hang core application logic on `extra`, raw responses or school code values. - Usage examples are in the [guides](https://cassiiopeia.github.io/pro-sejongpeia/en/guides/student-verification/). ## Changing this policy If the policy moves toward promising backward compatibility, this document is updated first and the release notes announce it. Share opinions in [Discussions](https://github.com/Cassiiopeia/pro-sejongpeia/discussions) or an issue. --- # FAQ Source: https://cassiiopeia.github.io/pro-sejongpeia/en/faq/ Section: More :::note[Read this first] - This is not an official Sejong University library, and it never stores passwords. - Call it from a server only. Calling it from a browser or app exposes the password. - If verification fails, check with the demo first. A locked account must be reset at the portal. ::: ## Is there a Sejong University login API? Yes. pro-sejongpeia is a Java, Python and Node.js library that handles the Sejong University portal login (SSO) for you and also provides school data that needs no login. With a student ID and password it returns enrollment status, major, year, classic-reading certification and contact info. See the [introduction](https://cassiiopeia.github.io/pro-sejongpeia/en/start/introduction/) and the [quick start](https://cassiiopeia.github.io/pro-sejongpeia/en/start/quickstart/). ## Is my password stored? No. The library only uses the password for the portal login request. The live demo server also returns the result without storing input. In your own service, never log or persist passwords. ## Is this an official Sejong University library? No. It is a personal open-source project by a Sejong student and is not affiliated with the university. If the portal changes it may stop working. ## What are the requirements? | Language | Requirements | |---|---| | Java | Java 17 or later, Spring Boot 3.x (it can also be used without Spring) | | Python | Python 3.9 or later, no dependencies beyond the standard library | | Node.js | Node 18 or later, zero dependencies, TypeScript types included, ESM and CommonJS | ## Are the results the same in all three languages? Which should I use? Features, error codes, result fields and value rules are the same and tests enforce it. Only the naming style (`snake_case` / `camelCase`) and a few language-specific features differ. Pick the language that matches your stack. Details are in [API stability](https://cassiiopeia.github.io/pro-sejongpeia/en/api-stability/). ## Authentication suddenly fails Try the [live demo](https://cassiiopeia.github.io/pro-sejongpeia/en/#demo) first. If the demo also fails, the portal likely changed. Check the error **code**, not the message ([Error codes](https://cassiiopeia.github.io/pro-sejongpeia/en/auth/error-codes/)), and report it in an issue with the error code. Never post a student ID or password. The steps are in [Issues and pull requests](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/issues-and-prs/). ## My portal account is locked After several wrong passwords the portal locks the account and the library returns `ACCOUNT_LOCKED`. Reset the password at after identity verification. Waiting does not unlock it, and the library cannot unlock it for you. The library never retries a rejected login and stops at the first failure. If anyone can type a student ID into your service, limit failed attempts per student ID. ([Login verdict and account lock protection](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/login-verdict/)) ## An empty list came back: is there no data, or is the school having a problem? They cannot be told apart. Public data does not throw when one source fails: it returns the last cached value or an empty list. Show a message such as "could not load" next to the result. ([Failure isolation and cache](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/failure-isolation-cache/)) ## How often is school data refreshed? Results are cached in memory, 24 hours by default (`cache_ttl_seconds` defaults to 86400). A fresh cache entry means no request to the school, and identical concurrent requests share one call. Create one `SejongPublicData` per process. ([Common rules](https://cassiiopeia.github.io/pro-sejongpeia/en/data/overview/)) ## Can I call it from a browser or an app? No. Call it from a server only. Calling it directly from a browser or app exposes the password to third-party code. The Node.js package is server-side only too. ## I think the portal changed. How do I report it? A check logs in to the real portal every day and opens an issue automatically when something is wrong. If you noticed first, report it in an [issue](https://github.com/Cassiiopeia/pro-sejongpeia/issues/new/choose) with the error code and the method you called. ([Daily live portal check](https://cassiiopeia.github.io/pro-sejongpeia/en/understand/daily-live-check/)) ## Where do I report a security problem? Use GitHub's [private reporting](https://github.com/Cassiiopeia/pro-sejongpeia/security/advisories/new), not a public issue. Never include real student IDs, passwords or other personal data. Details are in [Issues and pull requests](https://cassiiopeia.github.io/pro-sejongpeia/en/contribute/issues-and-prs/). ## What is the license? [MIT](https://github.com/Cassiiopeia/pro-sejongpeia/blob/main/LICENSE). The rights to the Sejong University seal belong to Sejong University.