Skip to content

Add HTTP caching, timetable slot flags, and NEIS schedule integration - #17

Merged
injoon5 merged 6 commits into
mainfrom
claude/trusting-mayer-pw50py
Oct 2, 2026
Merged

injoon5 merged 6 commits into
mainfrom
claude/trusting-mayer-pw50py

Conversation

@injoon5

@injoon5 injoon5 commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Summary

This PR adds HTTP caching with ETags, enriches timetable periods with cancelled and makeup flags, integrates NEIS school calendar events into the /timetable response, and introduces helper utilities for working with Korean school schedules.

Key Changes

HTTP Caching (src/http/cache.ts, src/test/http-cache.ts)

  • Implemented strong ETag generation (SHA-256 base64url) and If-None-Match matching with weak comparison support
  • Added Cache-Control headers with CDN-friendly s-maxage values:
    • /timetable: 5 minutes
    • /lunch and /schedule: 6 hours
    • /school and /classes: 1 day
    • 404 NEIS_DATA_NOT_FOUND: 1 hour (cacheable error)
    • Other errors: no-store
  • Created httpCache() Elysia hook for automatic 304 Not Modified responses when ETags match
  • Comprehensive unit tests covering tag matching, weak comparison, and real app integration

Timetable Enhancements (src/services/timetable-extras.ts, src/test/timetable-extras.ts)

  • Slot flags: Added cancelled (replaced with empty subject) and makeup (subject starts with [보강]) boolean flags to timetable periods
  • Week helpers: Exported weekStartYmd(), addDaysYmd(), kstYmd(), and weekYmdRange() for KST-aware date calculations
  • Grade filtering: Implemented appliesToGrade() and filterScheduleByGrade() to filter NEIS SchoolSchedule rows by grade (1–6)
  • Schedule mapping: Created mapScheduleToDays() to convert NEIS rows into day objects with closed status and event lists
  • Schedule fetching: Added fetchWeekSchedule() to fetch Mon–Sat calendar events with graceful error handling (empty array on no data, undefined on network failure)
  • Deadline utility: Implemented withDeadline() for timeout-based promise resolution

/timetable Response Updates

  • Added week_start field (YYYYMMDD Monday in KST) to responses
  • Added optional days array (index-aligned with timetable) containing:
    • date: YYYYMMDD
    • closed: true for holidays/closures
    • events: grade-filtered NEIS calendar event names
  • Updated day_time format from "HH:MM" to "<period>(HH:MM)" (e.g., "1(08:10)")
  • Each period now includes cancelled and makeup flags
  • Schedule join runs in parallel with timetable fetch; omitted if it exceeds 1-second deadline

/schedule Endpoint Enhancement

  • Added optional grade query parameter (1–6) to filter SchoolSchedule rows by grade
  • Updated OpenAPI schema and documentation

Schema & Documentation Updates

  • Updated TimetablePeriodSchema with new cancelled and makeup fields
  • Added TimetableDaySchema for the new days array
  • Updated TimetableResponseSchema with week_start and days
  • Enhanced OpenAPI descriptions for caching behavior and new fields
  • Updated client documentation with KST week helper examples

Integration (src/app.ts)

  • Wired httpCache() hook to /school and /classes endpoints
  • Integrated schedule fetching into /timetable handler with 1-second grace period
  • Added error cache control headers to error responses

Notable Implementation Details

  • All dates are YYYYMMDD in Asia/Seoul timezone, regardless of server location
  • NEIS 토요휴업일 (Saturday holiday) is filtered out from days to avoid redundant Saturday entries
  • Duplicate event names on the same day are deduplicated in the events array
  • Schedule fetch failures (network errors) gracefully degrade by omitting days rather than

https://claude.ai/code/session_01DvQgaoFCY6wDChdDYQbV8D

claude added 6 commits October 2, 2026 05:02
Every 200 JSON response from /timetable, /lunch, /schedule, /school and
/classes now carries a strong ETag (SHA-256 of the serialised body). A
matching If-None-Match (weak comparison, lists and * supported) gets a
304 with an empty body. Cache-Control keeps max-age=0 and adds per-route
s-maxage so the Vercel CDN can absorb repeated polls; 404
NEIS_DATA_NOT_FOUND is cacheable for an hour and every other error is
no-store. Status codes and bodies are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvQgaoFCY6wDChdDYQbV8D
/timetable now returns week_start, the YYYYMMDD Monday of the week the
response describes, computed in Asia/Seoul so the UTC server agrees with
Korean clients. Clients that cache week=1 can tell when that cached
"next week" has become this week. Reuses the client's weekYmdRange,
now exported from @timeforschool/client/timetable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvQgaoFCY6wDChdDYQbV8D
Each /timetable slot gains two booleans, appended after `original`:
- cancelled: replaced is true and subject is empty (exam-day lessons
  that were dropped; `original` keeps the lesson)
- makeup: subject starts with "[보강]"

subject, teacher, replaced and original are unchanged, so existing
clients that ignore unknown keys see the same data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvQgaoFCY6wDChdDYQbV8D
/timetable now returns `days`, index-aligned with `timetable`:
{ date, closed, events }. It comes from SchoolSchedule for Mon–Sat of
week_start, started before the timetable fetch so both upstream calls
overlap, and reuses the route's NeisClient.

- Only rows whose *_GRADE_EVENT_YN for the requested grade is "Y" count.
- closed: a kept row is 공휴일 or 휴업일. events: kept EVENT_NMs,
  without 토요휴업일.
- An empty week returns closed:false / events:[] for every day.
- A failed or slow (>1s after the timetable is ready) schedule lookup
  omits `days`; the timetable still answers 200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvQgaoFCY6wDChdDYQbV8D
GET /schedule accepts an optional grade (1–6). When set, only rows whose
matching *_GRADE_EVENT_YN is "Y" are returned, in their raw NEIS shape.
An empty result after filtering is still 404 NEIS_DATA_NOT_FOUND (with
`grade` added to its details). Without grade, behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvQgaoFCY6wDChdDYQbV8D
- day_time: the example was "09:00"; the real format is
  "<period>(HH:MM)" (e.g. "1(08:10)"), start times only, and the array
  is empty when Comcigan has none.
- original: documented as {period, subject, teacher} or null, with the
  exam-day cancelled lesson as an example. Validation is unchanged.
- Timetable example now shows a cancelled and a make-up period.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvQgaoFCY6wDChdDYQbV8D
@vercel

vercel Bot commented Oct 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
school-api Ready Ready Preview Oct 2, 2026 5:11am UTC
school-api-docs Ready Ready Preview Oct 2, 2026 5:11am UTC

@injoon5
injoon5 merged commit e4511d0 into main Oct 2, 2026
5 checks passed

This branch was successfully deployed

2 active deployments
Preview – school-api — f13bdb15 Deployed Oct 2, 2026 by vercel[bot]
Preview – school-api-docs — f13bdb15 Deployed Oct 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants