Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/docs/content/docs/api/schedule.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ _openapi:
contents:
- content: >-
Returns NEIS SchoolSchedule rows between startdate and enddate
(inclusive).
(inclusive), in their raw NEIS shape. Optional `grade` keeps only rows
whose matching `*_GRADE_EVENT_YN` column (`ONE_GRADE_EVENT_YN` for
grade 1 … `SIX_GRADE_EVENT_YN` for grade 6) is `Y`. An empty result,
with or without `grade`, is `404 NEIS_DATA_NOT_FOUND`.
heading: school-calendar-events
---

Expand Down
6 changes: 5 additions & 1 deletion apps/docs/content/docs/api/timetable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ _openapi:
week, 1 = next week. Comcigan substitutions use `replaced` +
`original`. `day_time` always comes from Comcigan and is blank when
Comcigan has none. `teacher` also stays blank when Comcigan has no
data.
data. `week_start` is the KST Monday the response describes. Each
period carries `cancelled` and `makeup` flags. `days` (index-aligned
with `timetable`) marks holidays and lists the grade's NEIS calendar
events; it is fetched in parallel and omitted if the calendar lookup
fails.
heading: weekly-class-timetable
---

Expand Down
14 changes: 14 additions & 0 deletions apps/docs/content/docs/client/timetable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ const grade1Class3 = table.timetable[1][3].slice(1);

HTTP `GET /timetable` already returns that weekday array as `timetable`.

## KST week helpers

`@timeforschool/client/timetable` also exports the date helpers `fetchNeisTimeTable` uses. All dates are `YYYYMMDD` in Asia/Seoul, whatever the host time zone.

```ts
import { addDaysYmd, kstYmd, weekYmdRange } from "@timeforschool/client/timetable";

kstYmd(); // today in KST, e.g. "20261002"
weekYmdRange(1); // { mondayYmd: "20261005", fromYmd: "20261005", toYmd: "20261011" }
addDaysYmd("20261231", 1); // "20270101"
```

HTTP `GET /timetable` returns the same Monday as `week_start`.

## What NEIS does not publish

Left blank on purpose when Comcigan has no data (same keys, empty values):
Expand Down
199 changes: 186 additions & 13 deletions apps/docs/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "TimeforSchool API",
"description": "TimeforSchool wraps **Comcigan** and the Korean **NEIS Open API** (school info, classes, meals, calendar, timetables).\n\n### Timetable source\n`GET /timetable` defaults to `source=auto`: both upstreams in parallel, then merge per period (whichever side has a subject; shorter name when both do). Missing weekdays/periods fill from the other. NEIS Saturday is ignored. Pin `source=comcigan` or `source=neis` to call only one.\n\n### School identifier\nUse **either** `schoolname` **or** `schoolcode` (NEIS 7-digit code)—never both. No endpoint defaults to a particular school; `GET /school` requires `schoolname`.\n\n### Dates\n`startdate` / `enddate` use **YYYYMMDD** (e.g. `20250526`).\n\n### Errors\nFailed requests return `{ ok: false, error: { code, message, details? } }` with an appropriate HTTP status.",
"description": "TimeforSchool wraps **Comcigan** and the Korean **NEIS Open API** (school info, classes, meals, calendar, timetables).\n\n### Timetable source\n`GET /timetable` defaults to `source=auto`: both upstreams in parallel, then merge per period (whichever side has a subject; shorter name when both do). Missing weekdays/periods fill from the other. NEIS Saturday is ignored. Pin `source=comcigan` or `source=neis` to call only one.\n\n### School identifier\nUse **either** `schoolname` **or** `schoolcode` (NEIS 7-digit code)—never both. No endpoint defaults to a particular school; `GET /school` requires `schoolname`.\n\n### Dates\n`startdate` / `enddate` use **YYYYMMDD** (e.g. `20250526`).\n\n### HTTP caching\nEvery `200` from `/timetable`, `/lunch`, `/schedule`, `/school` and `/classes` carries a strong `ETag` (SHA-256 of the body). Send it back as `If-None-Match` to get `304 Not Modified` with an empty body when nothing changed. `Cache-Control` keeps `max-age=0` (clients decide when to ask) and sets `s-maxage` for the CDN: 5 minutes for `/timetable`, 6 hours for `/lunch` and `/schedule`, 1 day for `/school` and `/classes`, 1 hour for `404 NEIS_DATA_NOT_FOUND`. Other errors are `no-store`.\n\n### Errors\nFailed requests return `{ ok: false, error: { code, message, details? } }` with an appropriate HTTP status.",
"version": "0.0.1"
},
"tags": [
Expand Down Expand Up @@ -246,7 +246,7 @@
"Timetable"
],
"summary": "Weekly class timetable",
"description": "Fetches the class schedule. Default `source=auto` calls Comcigan and NEIS in parallel and merges them per period: whichever side has a subject is used, the shorter name wins when both do, and missing weekdays/periods fill from the other source. Empty or cancelled Comcigan periods still fill from NEIS. NEIS Saturday is ignored (stale 토요휴업일). Pin `source=comcigan` or `source=neis` to hit a single upstream. Provide schoolname, or schoolcode alone. week: 0 = this week, 1 = next week. Comcigan substitutions use `replaced` + `original`. `day_time` always comes from Comcigan and is blank when Comcigan has none. `teacher` also stays blank when Comcigan has no data.",
"description": "Fetches the class schedule. Default `source=auto` calls Comcigan and NEIS in parallel and merges them per period: whichever side has a subject is used, the shorter name wins when both do, and missing weekdays/periods fill from the other source. Empty or cancelled Comcigan periods still fill from NEIS. NEIS Saturday is ignored (stale 토요휴업일). Pin `source=comcigan` or `source=neis` to hit a single upstream. Provide schoolname, or schoolcode alone. week: 0 = this week, 1 = next week. Comcigan substitutions use `replaced` + `original`. `day_time` always comes from Comcigan and is blank when Comcigan has none. `teacher` also stays blank when Comcigan has no data. `week_start` is the KST Monday the response describes. Each period carries `cancelled` and `makeup` flags. `days` (index-aligned with `timetable`) marks holidays and lists the grade's NEIS calendar events; it is fetched in parallel and omitted if the calendar lookup fails.",
"parameters": [
{
"name": "schoolname",
Expand Down Expand Up @@ -538,7 +538,7 @@
"Schedule"
],
"summary": "School calendar events",
"description": "Returns NEIS SchoolSchedule rows between startdate and enddate (inclusive).",
"description": "Returns NEIS SchoolSchedule rows between startdate and enddate (inclusive), in their raw NEIS shape. Optional `grade` keeps only rows whose matching `*_GRADE_EVENT_YN` column (`ONE_GRADE_EVENT_YN` for grade 1 … `SIX_GRADE_EVENT_YN` for grade 6) is `Y`. An empty result, with or without `grade`, is `404 NEIS_DATA_NOT_FOUND`.",
"parameters": [
{
"name": "schoolname",
Expand Down Expand Up @@ -591,6 +591,35 @@
],
"type": "string"
}
},
{
"name": "grade",
"in": "query",
"required": false,
"schema": {
"minimum": 1,
"maximum": 6,
"description": "Optional grade level (학년). When set, only rows whose matching `*_GRADE_EVENT_YN` is `Y` are returned.",
"examples": [
1
],
"anyOf": [
{
"format": "numeric",
"default": 0,
"type": "string"
},
{
"minimum": 1,
"maximum": 6,
"description": "Optional grade level (학년). When set, only rows whose matching `*_GRADE_EVENT_YN` is `Y` are returned.",
"examples": [
1
],
"type": "number"
}
]
}
}
],
"responses": {
Expand Down Expand Up @@ -986,22 +1015,27 @@
"required": [
"day_time",
"timetable",
"update_date"
"update_date",
"week_start"
],
"properties": {
"day_time": {
"description": "Period start times from Comcigan. Empty when only NEIS data is available (NEIS does not publish bell times).",
"description": "Bell times from Comcigan, one string per period in the form `\"<period>(HH:MM)\"`, e.g. `\"1(08:10)\"`. Start times only, no end times. Always taken from Comcigan, so it is an empty array when Comcigan has none (`source=neis`, schools not on Comcigan, or Comcigan unreachable). NEIS does not publish bell times.",
"examples": [
[
"09:00",
"09:50",
"10:00"
"1(08:10)",
"2(09:10)",
"3(10:10)",
"4(11:10)",
"5(13:10)",
"6(14:10)",
"7(15:10)"
]
],
"type": "array",
"items": {
"examples": [
"09:00"
"1(08:10)"
],
"type": "string"
}
Expand All @@ -1015,14 +1049,18 @@
"subject": "수학",
"teacher": "김교사",
"replaced": false,
"original": null
"original": null,
"cancelled": false,
"makeup": false
},
{
"period": 2,
"subject": "체육",
"teacher": "이교사",
"replaced": false,
"original": null
"original": null,
"cancelled": false,
"makeup": false
},
{
"period": 3,
Expand All @@ -1033,7 +1071,31 @@
"period": 3,
"subject": "영어",
"teacher": "박교사"
}
},
"cancelled": false,
"makeup": false
},
{
"period": 4,
"subject": "",
"teacher": "",
"replaced": true,
"original": {
"period": 4,
"subject": "국사",
"teacher": "김기*"
},
"cancelled": true,
"makeup": false
},
{
"period": 5,
"subject": "[보강]음악 연주와 창작",
"teacher": "",
"replaced": false,
"original": null,
"cancelled": false,
"makeup": true
}
]
]
Expand All @@ -1048,7 +1110,9 @@
"subject",
"teacher",
"replaced",
"original"
"original",
"cancelled",
"makeup"
],
"properties": {
"period": {
Expand Down Expand Up @@ -1078,6 +1142,15 @@
"type": "boolean"
},
"original": {
"description": "The lesson Comcigan originally scheduled for this slot: an object `{period, subject, teacher}` when `replaced` is true, otherwise `null`. Always `null` for `source=neis`. When the lesson was cancelled (e.g. on exam days), `subject` is empty, `cancelled` is true and `original` names the dropped lesson.",
"examples": [
null,
{
"period": 1,
"subject": "국사",
"teacher": "김기*"
}
],
"anyOf": [
{
"type": "object",
Expand Down Expand Up @@ -1126,6 +1199,20 @@
}
}
]
},
"cancelled": {
"description": "true when the lesson was cancelled: `replaced` is true and `subject` is empty (e.g. exam days). `original` holds the cancelled lesson.",
"examples": [
false
],
"type": "boolean"
},
"makeup": {
"description": "true for a make-up lesson: `subject` starts with `[보강]`. `subject` is left as is, prefix included.",
"examples": [
false
],
"type": "boolean"
}
}
}
Expand All @@ -1137,6 +1224,92 @@
"2025-05-26"
],
"type": "string"
},
"week_start": {
"pattern": "^\\d{8}$",
"description": "Monday of the week this response describes, as YYYYMMDD in KST. `week=0` is the current Mon–Sun week in Korea, `week=1` the one after. Keep it with a cached response: a cached `week=1` whose `week_start` is now the current Monday has become this week.",
"examples": [
"20261005"
],
"type": "string"
},
"days": {
"description": "Per-day status from NEIS SchoolSchedule, index-aligned with `timetable` (`days[i]` describes `timetable[i]`, starting at `week_start`). Only rows that apply to the requested grade count. A week with no schedule rows returns every day with `closed: false` and `events: []`. **Omitted** when the schedule lookup fails or is slow; the timetable is still returned.",
"examples": [
[
{
"date": "20261005",
"closed": true,
"events": [
"대체공휴일"
]
},
{
"date": "20261006",
"closed": false,
"events": []
},
{
"date": "20261007",
"closed": false,
"events": [
"중간고사"
]
},
{
"date": "20261008",
"closed": false,
"events": [
"중간고사"
]
},
{
"date": "20261009",
"closed": true,
"events": [
"한글날"
]
}
]
],
"type": "array",
"items": {
"type": "object",
"required": [
"date",
"closed",
"events"
],
"properties": {
"date": {
"pattern": "^\\d{8}$",
"description": "Calendar date (YYYYMMDD) of this timetable day.",
"examples": [
"20261009"
],
"type": "string"
},
"closed": {
"description": "true when NEIS SchoolSchedule marks the day as a holiday (`공휴일`) or school closure (`휴업일`) for this grade. The timetable for that day may still repeat the holiday name in every period.",
"examples": [
true
],
"type": "boolean"
},
"events": {
"description": "SchoolSchedule `EVENT_NM`s for this day that apply to the requested grade, without `토요휴업일`. Empty when there are none.",
"examples": [
[
"한글날"
]
],
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"$id": "#/components/schemas/TimetableResponse"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"openapi:sync": "npm run openapi:export && npm run openapi:generate -w @timeforschool/docs",
"dev": "tsx watch src/server.ts",
"start": "node dist/server.js",
"test": "tsx src/test/timetable-merge.ts",
"test": "tsx src/test/timetable-merge.ts && tsx src/test/http-cache.ts && tsx src/test/timetable-extras.ts",
"test:live": "tsx src/test/smoke.ts && tsx src/test/yangjeong-1-3.ts && tsx src/test/neis-timetable.ts",
"vercel:dev": "vercel dev",
"vercel:deploy": "vercel deploy --prod"
Expand Down
8 changes: 7 additions & 1 deletion packages/client/src/timetable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ export type {
TimeTableResult,
TimetableSource,
} from "./types.js";
export { fetchNeisTimeTable, mapNeisTimetableRows } from "./neis.js";
export {
addDaysYmd,
fetchNeisTimeTable,
kstYmd,
mapNeisTimetableRows,
weekYmdRange,
} from "./neis.js";
export { mergeTimeTableResults, mergePeriod } from "./merge.js";
export type { FetchNeisTimeTableOptions } from "./neis.js";
export {
Expand Down
Loading
Loading