Inspiration
I have made map apps before. Every toilet finder I know shows pins. None of them tells you the one thing you need at 10 pm with a small child: is that door open right now?
The reason is in the data. Korea's national public-toilet dataset has an opening-hours column, but it is free text. 33,820 rows, 589 different ways of writing the hours. "24시간", "09:00~18:00", "(평일)09:00~18:00+(주말)09:00~18:00", "-", "8시간". So apps skip the column, and people walk to locked doors. I wanted one small page that actually reads it.
What it does
One page. Tap "Use my location" or a demo spot, and you get three cards, nearest first. Each card shows the distance, the hours exactly as written in the dataset, and one badge: Open, Closed, or Hours unknown. A closed toilet turns grey, both the card and the pin on the map.
Under the cards there is a time dial. Drag it to 22:00 and the colours change without reloading. The footer is not a slogan, it is a count from the shipped data: at 22:00, 7,754 of the 22,500 toilets with readable hours are closed. That is 34.5%. At 15:00 it is 60.
When the reader cannot understand the hours, the badge says "Hours unknown". It never guesses, because a wrong "open" sends someone to a locked door.
How I built it
I started from an empty folder with the Devpost Learn skill pack (npx skills add challengepost/learn-ai-basics --all -y) in Claude Code. The agent went through the skills in order, 1-start to 5-build, and wrote devpost/scope.md, prd.md, spec.md and checklist.md from the pack's templates.
One honest note: the skills are written as a live interview. In my case the interview answers came from a written brief I gave for this hackathon (the idea, the "closed = grey" kick, no paid APIs, no copied code). I reviewed and approved the four documents before submitting. The same sentence is in the README.
The build followed the checklist's four slices in order: the hours parser and the stats script first, because that was the only real risk, then the nearest-3 API, then the page, then the time dial and the error states. Each slice has tests and a commit.
The stack is on purpose very small. Node.js, zero npm dependencies (own CSV reader, static server, haversine), one HTML page with Leaflet 1.9.4 and OpenStreetMap tiles. The hours reader (src/lib/hours.mjs) runs on the server for the statistics and in the browser for the dial, so there is only one definition of "open". The data is the national public-toilet standard dataset from data.go.kr (snapshot 2026-10-18, no usage restriction), shipped unchanged with its SHA-256. 14 tests run with node --test.
Challenges I ran into
- Free-text hours. "24시간", "09시~18시", "0900~1800", "05:00~01:00" (overnight), day-tagged strings, and traps like "하절기(7~9월)", which a naive regex reads as 07:00 to 09:00. The parser handles the common shapes, takes the first readable range for day-tagged rows (written down as a simplification in spec.md), and returns unknown for the rest. 92.5% of toilets with coordinates come out readable, 7.5% say unknown.
- The data picked the demo. I planned Seoul City Hall. Its three nearest toilets are subway toilets open 05:00 to 01:00, so nothing ever turned grey. Busan Station was all "24시간". I probed the data and moved the demo spots to Jeonju Hanok Village, Hongdae and Gwangju City Hall. It is recorded under checklist.md > Revisions.
- Duplicates. 2,705 rows share a name and coordinates, so the first API call returned the same toilet twice. The loader keeps one.
Accomplishments that I'm proud of
- The grey is a fact, not a design choice. Every badge comes from the same parser that produced the footer numbers.
- "Hours unknown" is a real state, not an error.
- Zero dependencies, no keys, no database. node src/server.mjs and it runs, 24,321 toilets loaded in about 150 ms.
- The plan changed four times during the build, and every change is written next to the reason.
What I learned
- Plan first, then let the data correct the plan. Writing scope, PRD and spec before code made the kernel clear: grey means closed. That is why the first slice was the parser and the stats script, not the map. Running the stats early showed the month-span false positive and the duplicates before any UI existed.
- Pick a demo with data, not with a feeling. The spot I was sure about did not work. A five-line probe found three that do.
- Keep one source of truth. Sharing hours.mjs between server and browser means the footer count and the badge on screen can never disagree.
What's next for Where's the Loo
- Weekday and weekend logic for day-tagged hours (today: first readable range).
- Walking distance instead of straight-line, once a routing API is acceptable.
- A scheduled refresh from the portal instead of a shipped snapshot.
- A phone-sized shortcut: one tap, one answer, nearest open toilet.
Built With
- claude-code
- css3
- csv
- data.go.kr
- devpost-learn-skill-pack
- html5
- javascript
- leaflet.js
- node.js
- openstreetmap
Log in or sign up for Devpost to join the conversation.