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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# NEIS Open API key — https://open.neis.go.kr/
# Optional. Anonymous NEIS requests work; a key raises the rate limit.
# https://open.neis.go.kr/
NEIS_API_KEY=

# Comma-separated origins. Defaults: timetable.injoon5.com, docs.timefor.school, localhost:3000
CORS_ORIGINS=
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Agent instructions (TimeForSchool monorepo)
# Agent instructions (TimeforSchool monorepo)

Instructions for Cursor Cloud Agents and other automation working in this repository.

Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TimeForSchool API
# TimeforSchool API

TypeScript API for NEIS school data (school info, classes, lunch, schedule) and Comcigan timetables.
TypeScript API for NEIS school data (school info, classes, lunch, schedule) and weekly timetables (Comcigan + NEIS, merged by default).

Production: https://api.timefor.school

Expand All @@ -11,7 +11,7 @@ Built with [Elysia](https://elysiajs.com). Interactive reference UI is [Scalar](
| Package | Path | Description |
|---------|------|-------------|
| `timeforschool-api` | repo root | HTTP API (`src/`) |
| `@timeforschool/client` | `packages/client/` | NEIS + Comcigan client library (publishable to npm) |
| `@timeforschool/client` | `packages/client/` | NEIS + Comcigan/NEIS timetable client (publishable to npm) |
| `@timeforschool/docs` | `apps/docs/` | Fumadocs site (client + OpenAPI reference) |

## Documentation site
Expand Down Expand Up @@ -120,8 +120,9 @@ Errors use a consistent JSON shape and HTTP status code:
| `MISSING_SCHOOL_IDENTIFIER` | 400 | Neither `schoolname` nor `schoolcode` sent |
| `SCHOOL_NOT_FOUND` | 404 | NEIS has no matching school |
| `NEIS_DATA_NOT_FOUND` | 404 | NEIS returned no rows for the date range |
| `TIMETABLE_INVALID_GRADE_CLASS` | 404 | No Comcigan data for grade/class |
| `TIMETABLE_AMBIGUOUS_SCHOOL` | 409 | Multiple Comcigan matches—use `schoolcode` |
| `TIMETABLE_SCHOOL_NOT_FOUND` | 404 | Neither Comcigan nor NEIS found the school |
| `TIMETABLE_INVALID_GRADE_CLASS` | 404 | No timetable for grade/class |
| `TIMETABLE_AMBIGUOUS_SCHOOL` | 409 | Multiple Comcigan matches—use `schoolcode`. `source=auto` still 409s if Comcigan is ambiguous. |
| `NEIS_UPSTREAM_ERROR` | 502 | NEIS API failure |
| `TIMETABLE_UPSTREAM_ERROR` | 502 | Comcigan fetch/parse failure |
| `INTERNAL_ERROR` | 500 | Unexpected error |
Expand All @@ -133,7 +134,7 @@ Errors use a consistent JSON shape and HTTP status code:
| `GET /` | Service metadata and doc links |
| `GET /school` | School info (`schoolname`) |
| `GET /classes` | Class numbers (`grade`, `schoolname` or `schoolcode`) |
| `GET /timetable` | Weekly timetable (`grade`, `classno`, `week`, `schoolname` or `schoolcode`) |
| `GET /timetable` | Weekly timetable (`grade`, `classno`, `week`, `schoolname` or `schoolcode`; default `source=auto` merge) |
| `GET /lunch` | Meal menus (`startdate`, `enddate`, `schoolname` or `schoolcode`) |
| `GET /schedule` | School calendar (`startdate`, `enddate`, `schoolname` or `schoolcode`) |

Expand All @@ -146,7 +147,8 @@ Errors use a consistent JSON shape and HTTP status code:
- `npm run build` — build `@timeforschool/client`, then compile API to `dist/`
- `npm run build:client` — build client package only
- `npm start` — run compiled server (`node dist/server.js`)
- `npm test` — structural smoke tests
- `npm test` — unit tests (no network)
- `npm run test:live` — live NEIS/Comcigan smoke

## Deploy to Vercel

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { apiProductionUrl } from '@/lib/shared';
export default function HomePage() {
return (
<div className="flex flex-col justify-center text-center flex-1 px-6">
<h1 className="text-3xl font-bold mb-3">TimeForSchool</h1>
<h1 className="text-3xl font-bold mb-3">TimeforSchool</h1>
<p className="text-fd-muted-foreground mb-8 max-w-lg mx-auto">
Documentation for the TypeScript client and the production HTTP API
(NEIS + Comcigan).
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export const metadata: Metadata = {
process.env.NEXT_PUBLIC_DOCS_URL ?? 'https://docs.timefor.school',
),
title: {
default: 'TimeForSchool',
template: '%s | TimeForSchool',
default: 'TimeforSchool',
template: '%s | TimeforSchool',
},
description:
'Documentation for @timeforschool/client and the TimeForSchool HTTP API.',
'Documentation for @timeforschool/client and the TimeforSchool HTTP API.',
};

export default function Layout({ children }: LayoutProps<'/'>) {
Expand Down
13 changes: 8 additions & 5 deletions apps/docs/content/docs/api/timetable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ _openapi:
id: weekly-class-timetable
contents:
- content: >-
Fetches the class schedule from Comcigan. Provide schoolname, or
schoolcode alone (name is resolved via NEIS). week: 0 = this week, 1 =
next week. Most periods have `replaced: false` and `original: null`;
when a period was substituted, `replaced` is true and `original` is
the class before the change.
Fetches the class schedule. Default `source=auto` calls Comcigan and
NEIS in parallel and merges them: the shorter subject name wins, and
missing weekdays/periods fill from the other source. Cancelled
Comcigan periods are kept. 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`. NEIS-only fields
stay blank (`teacher`, `day_time`) when Comcigan has no data.
heading: weekly-class-timetable
---

Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/docs/client/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ description: Typed error classes from @timeforschool/client

| Class | When |
| ----- | ---- |
| `TimetableSchoolNotFoundError` | Comcigan school search empty |
| `TimetableAmbiguousSchoolError` | Multiple Comcigan matches |
| `TimetableSchoolNotFoundError` | Comcigan or NEIS school search empty |
| `TimetableAmbiguousSchoolError` | Multiple Comcigan matches — pass `schoolCode`. NEIS name duplicates keep the first row. |
| `TimetableParseError` | HTML/JSON parse failure |
| `TimetableInvalidWeekError` | `weekNum` not `0` or `1` |
| `TimetableError` | Base class |
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/content/docs/client/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ The [`@timeforschool/client`](https://www.npmjs.com/package/@timeforschool/clien

The production API at [api.timefor.school](https://api.timefor.school) mirrors these capabilities. Pass **`schoolname`** or **`schoolcode`** on endpoints that need a school—never both. No route defaults to a particular school (including `GET /school`, which requires `schoolname`).

`GET /timetable` defaults to **`source=auto`**: Comcigan and NEIS in parallel, shorter subject wins, weekday gaps fill from the other. Pin **`source=comcigan`** or **`source=neis`** to call one upstream. NEIS Saturday is ignored.

## Subpath exports

| Import | Use case |
| ------ | -------- |
| `@timeforschool/client` | Full surface (NEIS + timetable) |
| `@timeforschool/client/neis` | NEIS only |
| `@timeforschool/client/timetable` | Comcigan timetable only |
| `@timeforschool/client/timetable` | Comcigan / NEIS weekly timetable |

## Requirements

Expand All @@ -35,6 +37,7 @@ const school = schools[0];
const table = await fetchTimeTable({
schoolName: school.SCHUL_NM,
weekNum: 0,
key: process.env.NEIS_API_KEY,
});
```

Expand Down
30 changes: 26 additions & 4 deletions apps/docs/content/docs/client/neis.mdx
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
---
title: NEIS client
description: School info, classes, meals, and calendar via NeisClient
description: School info, classes, meals, calendar, and official timetables via NeisClient
---

```ts
import { NeisClient, NeisDataNotFoundError } from "@timeforschool/client";
// or: import { NeisClient } from "@timeforschool/client/neis";
```

Port of Python [neispy](https://github.com/SaidBySolo/neispy) against `https://open.neis.go.kr/hub`.

## Construction

```ts
const neis = new NeisClient({
key: process.env.NEIS_API_KEY, // optional; sample key used when omitted
key: process.env.NEIS_API_KEY, // optional; omitted requests are anonymous
pIndex: 1,
pSize: 100,
pSize: 1000,
});
```

Responses paginate automatically up to the reported `list_total_count`.

## Methods

| Method | NEIS endpoint | Notes |
| ------ | ------------- | ----- |
| `schoolInfo` | `schoolInfo` | Search by `SCHUL_NM` or codes |
| `classInfo` | `classInfo` | Requires office + school codes and `GRADE` |
| `classInfo` | `classInfo` | Requires office + school codes |
| `mealServiceDietInfo` | `mealServiceDietInfo` | `MLSV_FROM_YMD` / `MLSV_TO_YMD` as `YYYYMMDD` |
| `schoolSchedule` | `SchoolSchedule` | `AA_FROM_YMD` / `AA_TO_YMD` as `YYYYMMDD` |
| `acaInsTiInfo` | `acaInsTiInfo` | Academies / 교습소 |
| `elsTimetable` | `elsTimetable` | Elementary. Pre-2023 dates use `elsTimetablebgs` |
| `misTimetable` | `misTimetable` | Middle school |
| `hisTimetable` | `hisTimetable` | High school |
| `spsTimetable` | `spsTimetable` | Special schools |
| `schoolTimetable` | (dispatched) | Picks els/mis/his/sps from `SCHUL_KND_SC_NM` |
| `schoolMajorinfo` | `schoolMajorinfo` | High-school majors |
| `schulAflcoinfo` | `schulAflcoinfo` | Course tracks |
| `tiClrminfo` | `tiClrminfo` | Classrooms |

## Example

Expand All @@ -46,6 +59,15 @@ const meals = await neis.mealServiceDietInfo({
MLSV_TO_YMD: "20250526",
});

const his = await neis.hisTimetable({
ATPT_OFCDC_SC_CODE: school.ATPT_OFCDC_SC_CODE,
SD_SCHUL_CODE: school.SD_SCHUL_CODE,
TI_FROM_YMD: "20260511",
TI_TO_YMD: "20260515",
GRADE: "1",
CLASS_NM: "3",
});

try {
await neis.schoolSchedule({
ATPT_OFCDC_SC_CODE: school.ATPT_OFCDC_SC_CODE,
Expand Down
69 changes: 63 additions & 6 deletions apps/docs/content/docs/client/timetable.mdx
Original file line number Diff line number Diff line change
@@ -1,36 +1,93 @@
---
title: Timetable (Comcigan)
description: fetchTimeTable for weekly class schedules
title: Timetable
description: fetchTimeTable merges Comcigan + NEIS by default (shorter name wins)
---

```ts
import {
fetchTimeTable,
fetchNeisTimeTable,
TimetableAmbiguousSchoolError,
TimetableSchoolNotFoundError,
} from "@timeforschool/client";
// or: import { fetchTimeTable } from "@timeforschool/client/timetable";
```

Default `source: "auto"` calls Comcigan and NEIS in parallel, then merges:

- Shorter non-empty **subject** wins (Comcigan on a tie)
- Missing weekdays/periods fill from the other source
- Cancelled Comcigan periods (`replaced` + empty subject) are kept — not overwritten with NEIS placeholders like `여름방학`
- **NEIS Saturday is dropped.** Schools do not have Saturday class; leftover `토요휴업일` rows are stale

Pin `source: "comcigan"` or `source: "neis"` to hit a single upstream. NEIS is mapped onto the Comcigan result shape; fields the Open API does not publish stay blank. Comcigan output is never stripped to look like NEIS.

## Options

| Field | Type | Description |
| ----- | ---- | ----------- |
| `schoolName` | `string` | Korean school name (Comcigan search) |
| `schoolCode` | `string` | Optional NEIS 7-digit code for disambiguation |
| `schoolName` | `string` | Korean school name |
| `schoolCode` | `number` | Optional NEIS 7-digit code for disambiguation |
| `weekNum` | `0 \| 1` | `0` = this week, `1` = next week |
| `source` | `"auto" \| "comcigan" \| "neis"` | Default `auto` |
| `key` | `string` | NEIS API key (used for `auto` and `neis`) |

HTTP: `GET /timetable` (same `source` query).

## Example
## Shape

Both sources return `TimeTableResult` (the Comcigan grid). Slice `[1]` on the day axis to skip Sunday. Saturday is not included from NEIS.

```ts
const table = await fetchTimeTable({
schoolName: "양정고등학교",
weekNum: 0,
key: process.env.NEIS_API_KEY,
});

const grade1Class3 = table.timetable[1][3].slice(1);
```

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

## What NEIS does not publish

Left blank on purpose when Comcigan has no data (same keys, empty values):

| Field | NEIS value |
| ----- | ---------- |
| `teacher` | `""` |
| `dayTime` / HTTP `day_time` | `[]` |
| `original` | `null` |
| `replaced` | `false` |
| `homeroomTeachers` | `[]` |

`subject` comes from `ITRT_CNTNT`. `updateDate` is `LOAD_DTM` as `YYYY-MM-DD` when present.

## Example (auto merge)

```ts
const table = await fetchTimeTable({
schoolName: "목운중학교",
weekNum: 0,
key: process.env.NEIS_API_KEY,
});

// grade 1, class 3 — slice(1) skips Sunday slot
const grade1Class3 = table.timetable[1][3].slice(1);
console.log(table.dayTime, table.updateDate);
```

When multiple Comcigan schools match the name, pass `schoolCode` or handle `TimetableAmbiguousSchoolError`.

## Example (NEIS-only school)

외대부고 does not use Comcigan. Auto still works: Comcigan 404s, NEIS fills in. Official name is **용인한국외국어대학교부설고등학교** (`7531146`). `schoolname=외대부고` will not match NEIS.

```ts
const table = await fetchTimeTable({
schoolName: "용인한국외국어대학교부설고등학교",
schoolCode: 7531146,
key: process.env.NEIS_API_KEY,
weekNum: 0,
});
```
8 changes: 4 additions & 4 deletions apps/docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
title: Introduction
description: TimeForSchool API and TypeScript client for Korean school data
description: TimeforSchool API and TypeScript client for Korean school data
---

TimeForSchool provides a hosted HTTP API and an npm package for Korean school data:
TimeforSchool provides a hosted HTTP API and an npm package for Korean school data:

- **Comcigan + NEIS** — weekly class timetables (`source=auto` merge by default)
- **NEIS Open API** — school profiles, classes, lunch menus, calendars
- **Comcigan** — weekly class timetables

On the HTTP API, every endpoint that targets a school requires **`schoolname`** or **`schoolcode`** (NEIS 7-digit code). There is no default school—`GET /school` also requires `schoolname`.
On the HTTP API, every endpoint that targets a school requires **`schoolname`** or **`schoolcode`** (NEIS 7-digit code). There is no default school—`GET /school` also requires `schoolname`. `GET /timetable` defaults to **`source=auto`**. Pin **`source=comcigan`** or **`source=neis`** to call one upstream.

<Cards>
<Card title="TypeScript client" href="/docs/client" description="Use @timeforschool/client in Node or the browser" />
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/lib/shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const appName = 'TimeForSchool';
export const appName = 'TimeforSchool';
export const docsRoute = '/docs';
export const docsImageRoute = '/og/docs';
export const docsContentRoute = '/llms.mdx/docs';
Expand Down
Loading
Loading