Hangangjari System Overview

The full Hangangjari data flow, from external collection through the API to the iOS app and widgets

Hangangjari is an iOS app for quickly checking information needed before and during a visit to a Hangang park. Its park lists, parking status, widgets, and notifications help users answer four questions: “Is it okay to go now?” “Can I bring the car?” “Would another park be better?” and “What can I find nearby?”

It is hard to calculate those answers from scratch on every app screen. Parking status changes often, events and notices refresh on different schedules, and forecasts need to be calculated in advance. Widgets must be readable even when the app is not open.

Workers prepare external data, the API creates responses the app and widgets can read immediately, and clients handle presentation and interaction.

The Whole System in One Sentence

Hangangjari turns slow, empty, or late-changing public and official data into screen-ready values accompanied by their state.

Collection, normalization, response generation, and display use separate execution paths. External sources can fail and update on different schedules, while the app and widgets need to respond quickly at the moment the user sees them.

The user-facing API keeps the read work needed to build screens. Workers and CronJobs handle work that takes time or can fail, such as collection, forecasting, notifications, and translation.

App and Widget as the Read Surface

The app and widgets are the first surfaces users see. Users check the situation there and move into a map app when needed. A widget runs separately from the app and reads display values that the app has saved.

flowchart LR
  User["User<br/>Checks before and during a visit"] --> Widget["Widget<br/>Short glance"]
  User --> App["App<br/>Compare · Detail · Settings"]
  Widget --> App
  App --> Maps["Map app<br/>Start moving"]

  subgraph Device["iOS device"]
    App
    Widget
    Local["In-app cache<br/>Recent screen values"]
    Saved["Stored display values<br/>Shared by app and widget"]
  end

  App <--> Local
  App --> Saved
  Widget --> Saved
  App --> API["Screen-ready API response"]
  Widget -.-> API

The server prepares responses the app can display immediately, so drawing a screen does not require another check of every external source. The client handles display and interaction.

The widget moves differently from the app process. It reads stored display values first and asks the API for fresh values only when needed.

External Data Prepared in Advance

If external sources are called one after another after a user request arrives, the app inherits their speed and failures directly. In Hangangjari, workers collect and normalize data first. Postgres keeps reference data and history, while Redis holds current state and summaries that need fast reads.

flowchart LR
  Sources["Public/official sources<br/>Parking · Events · Notices · Facilities"] --> Workers["Workers / CronJob<br/>Collect · Normalize · Validate"]
  Workers --> Records[("Postgres<br/>Reference data · History")]
  Workers --> Fast[("Redis<br/>Fast lookup values")]

  Records --> Prepared["Forecasts · Notification candidates<br/>Precomputed"]
  Prepared --> Records
  Prepared --> Fast

  Records --> API["Screen-ready API response"]
  Fast --> API
  API --> Client["App · Widget"]

This separation prevents “no value” from collapsing into one failure. An external source failure, a stopped worker, an empty fast lookup, and missing reference data in Postgres require different operational checks even when they produce a similar screen state.

The Last Confirmed Value During an Incident

During an incident or delay, fallback distinguishes the last successful value from unavailable information. The last successful value carries its observation time and delayed state.

flowchart TD
  Open["Open app or widget"] --> Saved{"Stored display value<br/>exists?"}
  Saved -->|Yes| ShowSaved["Show last checked value<br/>with updated time"]
  Saved -->|No| Request["Request a new response"]

  Request --> Result{"Can a response<br/>be produced?"}
  Result -->|Yes| Fresh["Show fresh value<br/>with refresh state"]
  Result -->|Last successful value| Last["Show last successful value<br/>with delay state"]
  Result -->|No| Empty["Show unavailable state<br/>without overclaiming"]

In a parking app, an honest response matters as much as a fast response. Showing a 40-minute-old value as current may make the screen fast, but it can move the user in the wrong direction.

home-summary, forecasts, and widget snapshots carry refresh state and source state along with each value.

Roles That Expanded Beyond Parking

The product began with a small problem: checking remaining spaces in Hangang park parking lots on the web every time was inconvenient.

Using the app revealed that a parking check is followed by questions about park congestion, events, facilities, notices, weather, and public transit. Favorites, widgets, and notifications connect that information to short checks before and during a visit.

The roles therefore split in the order the product needed them.

  • The app creates the screen where the user makes a decision now.
  • The widget shows values that can be checked briefly before opening the app.
  • The API returns responses shaped for each screen.
  • Workers handle slow sources, failures, and different update schedules outside the user request time.
  • Postgres preserves reference data, history, and audit records.
  • Redis handles caches and supporting indexes that can be rebuilt if lost.

Redis stores query results that can be rebuilt from Postgres. Workers read external sources ahead of time so user requests do not wait on collection delays.

One Response for the First Screen

The current app and general park widget use home-summary for the first screen. It bundles parking, outing information, forecasts, and source-level refresh states into a shape that both clients can read directly.

home-summary reduces call count while giving the app and widget the same first-screen structure.

If the client calls parking, outing, and forecast APIs separately and combines them locally, each screen can interpret values differently. When the server sends a combined response, the app can focus more on display and interaction.

The response format coordinates cases where parking, events, and forecasts carry different refresh states.

Workers record external data and source state, and the API assembles screen-ready responses. The app and widgets can then display each value with its freshness without interpreting external response formats.

Comments

Comments

    Image preview