-
-
Before / after: a real school menu line with bare allergen numbers, and the same dish in LunchKey for one child
-
The school week in Vietnamese, one verdict per dish, the child's allergens in red
-
"Might contain milk - ask the school": real menu lines where a number or dish name is ambiguous
-
Start without Korean: school link (QR) plus the child's allergens
-
Same menu in Chinese
-
One-page fridge sheet in the parent's language, including "might contain" warnings
-
Audit on live menus from schools in all 17 provincial education offices
-
Clean architecture: domain - application - adapters - ui, enforced by a CI layer check
Inspiration
Every Korean school publishes its lunch like this: 새알심만두국 (1.2.5.6.9.10.15.16.18). That is a Korean dish name followed by bare numbers, and each number stands for one of 19 allergens.
South Korea now has 202,208 multicultural K-12 students (4.0%, a record high). In a 2012 survey of 27,679 Korean students, 6.8% had a doctor-diagnosed food allergy. Multiplying the two gives a rough estimate of about 13,700 multicultural children with a food allergy. The people LunchKey is for are the parents among them who handle school matters and can't read Korean. I found no published count of that group, so I don't claim one.
For those parents, checking a single day takes four steps, five days a week, in a second language:
- Read every Korean dish name.
- Find its numbers.
- Look each number up on a legend sheet.
- Compare them with the child's list.
Camera translation (Lens, Papago) doesn't solve this. It turns 달걀찜 (1.5) into "steamed egg (1.5)", and the numbers stay numbers.
I study in Changwon, an industrial city with many migrant and multicultural families. Every school in all 17 provinces uses the same menu system, so one tool can fix this nationwide.
What it does
- Start without Korean. A school or multicultural family center copies a link with the school already chosen and prints it as a QR code (with any free QR maker). The parent picks a language (English, Vietnamese, Chinese, Filipino, Japanese, Russian or Korean) and the child's allergens. Without the link, the parent pastes the school's Korean name from any school notice. The menu comes live from the Ministry of Education's NEIS open API.
- The week, number first. Each dish shows one of four verdicts, an explanation of its name, and every allergen number decoded, with the child's allergens in red. The interface and all allergen names are available in all 7 languages. Dish names are explained in English, Vietnamese and Chinese; Filipino, Japanese and Russian show the English explanation.
- ⛔ Contains: a printed number matches the child's allergen.
- ? Might contain X — ask the school: the card names the allergen and says why. This happens when:
- a number might be one of the child's codes (
깍두기(물2) (9.13),감자전2(베이컨)); - the dish name suggests the allergen but no number is printed (
우유, milk, for a milk-allergic child); - the line can't be read.
- ○ No numbers printed (plain rice, fruit): shown in grey, not green.
- ✓ None of your child's allergens listed: the only green.
A day turns green only when every dish is ✓.
- Fridge sheet, family link, siblings. Print a one-page sheet in your language, which includes the "might contain" warnings. The share link carries only the school and the allergens, in the #hash, so it never reaches a server and never includes the child's name. A link opened for a second child adds that child.
- Paste a menu: daycare menus use the same numbers and get the same checker.
How I built it
- Spec first, then tests, then code. SPEC.md has 12 acceptance criteria. The code follows clean architecture:
domain(allergens, parser, verdict, name hints, gloss) ←application(week view, headline, link merge) ←adapters(NEIS, profile hash) ←ui. A CI layer check fails if domain or application code imports adapters or the UI, or touches fetch, localStorage or the DOM. - Safety is structural.
- Any number from 1 to 19 left anywhere in a line is treated as possibly a code, unless it is part of a counting word like
10곡. - All circled-number styles and full-width forms are normalized.
- Allergen names written instead of numbers (
(난류),(게)) count as codes. - A cut-off list (
(5.6.) is marked unreadable. - Two dishes on one line are split outside brackets (
A(…)/B,A (…) B). If numbers appear only after the last of several dishes, the line is never shown green unless the others are plain side items such as rice or sauce.
- Any number from 1 to 19 left anywhere in a line is treated as possibly a code, unless it is part of a counting word like
- 75 tests, including a fuzz test. For every code N it builds 9 real dish bases × about 40 placements: 28 separators such as
,·ㆍ;~–※, plus in front of the name, inside notes, circled and superscript. That is about 7,000 generated lines, and none may produce ✓ for a child avoiding N. - Zero dependencies, no build step, $0 to run on GitHub Pages.
- The API surprise. I measured that keyless NEIS returns only the first 5 rows and ignores paging. So LunchKey asks one narrow question per week: one school, one week, lunch only, at most 5 rows. It keeps the week in memory and re-renders from there. Saved weeks are stored as raw text and re-parsed, so parser fixes reach them too. A free personal NEIS key (
?key=, removable) unlocks full paging. If keyless access ever changes, the fallback is a scheduled job that pre-fetches opted-in schools as static JSON behind the same adapter. - Dish names are explained, not machine-translated. They come from a glossary of 670+ Korean menu words, with word order natural to each language:
달걀찜becomes "steamed egg", "trứng hấp" or "蒸鸡蛋". Unknown parts are marked "(?)". English shows them romanized; Vietnamese and Chinese keep the Korean letters. A one-syllable match next to an unknown piece isn't trusted, so the name can't invent an ingredient.
Measured on real data
npm run audit reads live lunches from 170 distinct schools in all 17 provincial offices.
- Aug 31 – Oct 2, 2026: 160 schools, 3,580 lunches, 24,745 dish lines, each school-day counted once.
- May 2026, never used for tuning: 145 schools, 16,799 lines, 0.01% flagged unreadable (shown as "?").
- 411 hand-labeled lines, labeled by the author and cross-checked with a separate regex (no second labeler). Each set is enforced by tests:
- 150 lines from held-out May: 150/150 correct.
- 150 evenly sampled lines: 150/150 correct. Most of these are easy lines.
- All 111 distinct odd formats the audits found: none ever shows ✓ for a number that could be a code.
- Warning load. For a child avoiding only peanut, school days come out ⛔ 5%, ? 0.6%, ○ 85% (some dish had no numbers) and ✓ 9%. Amber "?" days stay under 2% for every allergen, so the warning keeps its meaning.
- What three AI-assisted review rounds caught, each now a regression test:
- "common seasoning #2" read as milk;
(20kg)read as allergen codes;된장국(5.6)/요구르트2,닭강정(5.6)·1,1미역국(5.6)and(난류)gave a false ✓.
Challenges
Real menus are messy: (1.2.5), 1.5.6.10 glued to the name, ⑮⑥, (1/5/6), two dishes on one line, portion sizes, "self-serve" notes, and menu numbering.
My first audit was wrong. Keyless paging silently repeated the same rows, so the numbers looked 10× bigger than they were. I rewrote the audit to count distinct schools and distinct school-days.
The hardest part was safety. Reviewers kept finding formats that slipped through my list of allowed patterns. I fixed it by flipping the rule: anything left over that could be a code is treated as one.
Accomplishments
A parent who can't read Korean can now do what the school expects of every parent: check the menu and tell their child what to skip. It works today for any public school in Korea, in 7 languages, and every number here can be re-run.
What I learned
- Measure on real national data, then again on a month you never looked at.
- In a safety tool, the wording is most of the UX: "none of your child's allergens listed", never "safe".
What's next
- A user study. I haven't run one yet; this is the honest gap. The plan is a pilot with a multicultural family support center in Changwon handing out the school-link QR. It would measure the minutes and mistakes it takes to check one day, compared with the legend sheet.
- Native-speaker review of the Vietnamese and Chinese glossary.
- More languages: Thai, Khmer and Mongolian.
- A calendar subscription.
AI disclosure
Built with AI coding assistance (Claude), including AI-assisted code review. Every number above comes from scripts in the repo.
Built With
- css3
- github
- github-actions
- html5
- javascript
- neis-open-api
- node.js
Log in or sign up for Devpost to join the conversation.