Skip to content

Latest commit

 

History

History

README.md

Vision: The wBus Frontend

Next.js React TypeScript Tailwind CSS

Vision is the user-facing frontend for the wBus project — a real-time bus tracking app for Wŏnju (원주), South Korea. It renders live bus positions on an interactive map, animates their movement along route polylines, and displays arrival predictions and timetables.

Architecture

The project follows Feature-Sliced Design (FSD) and is organized into five layers, each with a clear responsibility and strict import rules.

src/
├── app/                # Next.js App Router — pages, layouts, API routes, global styles
│   └── api/            # Server-side API routes (bus locations, arrivals, stops)
├── entities/           # Domain models and data access for each entity
│   ├── bus/            #   BusItem, BusStopArrival — types & helpers
│   ├── route/          #   RouteInfo, BusRouteFeature, BusSchedule — polyline service
│   └── station/        #   BusStop, StationLocation — station map access
├── features/           # Use-case-specific logic (hooks, derived state)
│   ├── live-tracking/  #   SWR hooks for live bus locations & arrivals
│   └── map-view/       #   Map view persistence (localStorage)
├── shared/             # Cross-cutting infrastructure
│   ├── api/            #   fetchAPI() — retries, timeout, error handling
│   ├── cache/          #   CacheManager — LRU cache with request deduplication
│   ├── config/         #   Centralized env config with sensible defaults
│   ├── context/        #   AppMapContext — global MapRef provider
│   ├── hooks/          #   Shared React hooks
│   ├── redis/          #   Redis client, public API wrappers
│   ├── ui/             #   Shared UI components
│   └── utils/          #   General utilities
└── widgets/            # Composite UI blocks
    ├── BusListSheet/   #   Route list, schedule display
    └── MapContainer/   #   Map + markers + polylines + popups

Import rule: widgets → features → entities → shared. Each layer only imports from layers below it.

Data Flow

Overview

Vision consumes two categories of data through a three-tier caching pipeline:

┌──────────────────────────────────────────────────────────────────────┐
│                        EXTERNAL DATA SOURCES                         │
│                                                                      │
│  apis.data.go.kr          Vercel Blob / public/data/   localStorage  │
│  (Live bus API)           (Static GeoJSON, schedules)  (View state)  │
└────────┬──────────────────────────┬────────────────────────┬─────────┘
         │                          │                        │
    ┌────▼────┐               ┌─────▼────────┐          ┌─────▼──────┐
    │  Redis  │               │ CacheManager │          │  Browser   │
    │ (3-600s)│               │ (LRU, dedup) │          │  Storage   │
    └────┬────┘               └─────┬────────┘          └────────────┘
         │                          │
    ┌────▼──────────────────────────▼──────────────────────────────────┐
    │                         API ROUTES (Server)                      │
    │  GET /api/bus/[routeId]          → CachedData<BusItem[]>         │
    │  GET /api/bus-arrival/[busStopId] → CachedData<BusStopArrival[]> │
    │  GET /api/bus-stops/[routeId]    → CachedData<RawBusStop[]>      │
    └────────────────────────┬─────────────────────────────────────────┘
                             │
    ┌────────────────────────▼─────────────────────────────────────────┐
    │                     CLIENT (SWR + Hooks)                         │
    │  useBusLocationData()  ─── 10s polling ──→ /api/bus/{routeId}    │
    │  useBusArrivalInfo()   ─── 10s polling ──→ /api/bus-arrival/{id} │
    │  useBusData()          ─── combines live data + static polylines │
    └──────────────────────────────────────────────────────────────────┘

Static Data

Generated by the sibling project Polly and stored in public/data/ (dev) or Vercel Blob (prod):

File Format Content
routeMap.json JSON Maps route names (e.g. "30") → array of route IDs
stationMap.json JSON All bus stops in the city with coordinates
polylines/{routeId}.geojson GeoJSON Route path as LineString + stop metadata
schedules/{routeName}.json JSON Weekday/weekend timetables per route

Static data is loaded on demand through CacheManager, which provides LRU eviction (50–100 items) and request deduplication — concurrent fetches for the same key share a single promise.

Live Data

Real-time bus positions and arrival info are fetched from the Korea Public Data Portal (apis.data.go.kr), proxied through server-side API routes. Next.js API routes are marked with export const dynamic = "force-dynamic" to bypass default static caching and ensure our caching layers handle all logic.

Two-Tier Caching Strategy:

1. Real-Time Data (Redis + CDN): To optimize the API and prevent timeouts from the public portal, the Redis caching layer (src/shared/redis/client.ts) implements several advanced strategies for frequently-changing data:

  • Smart Caching (3s TTL): Live bus positions/arrivals are cached for 3 seconds in Redis, with an additional 3 seconds of CDN edge caching via Cache-Control: public, s-maxage=3, stale-while-revalidate=3.
  • In-Flight Request Coalescing: Prevents Cache Stampedes (Thundering Herd). If a cache expires and multiple users request the same data simultaneously, only one outgoing request is made to the public API, and all concurrent requests await its resolution.
  • Stale Data Fallback: The public API is occasionally unstable. The Redis cache keeps expired entries for an extended period (e.g., 60 seconds). If the public API fails (with exponential backoff retries), the system catches the error and serves the older "stale" data instead of showing an error to the user, ensuring uninterrupted service.

2. Static Data (CDN Only): Rarely-changing data like bus stop coordinates and route stop lists bypass Redis entirely and rely on CDN edge caching:

  • CDN-Only Caching (1h edge): Static endpoints use Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 to cache for 1 hour at the CDN edge, with 24-hour stale tolerance.
  • No Redis Overhead: By skipping Redis for static data, we reduce Redis memory usage and latency.
API Route Caching Strategy TTL Data Source
/api/bus/[routeId] Redis + CDN 3 sec Bus location API
/api/bus-arrival/[busStopId] Redis + CDN 3 sec Arrival prediction API
/api/bus-stops/[routeId] CDN only 1 hour Route stop list API
/api/route-stops/[routeName] CDN only 1 hour Static data files

The Redis layer implements a "smart cache" — when one user's request triggers a fetch, the result is cached for all later users within the TTL window.

Client-side polling (SWR):

  • Polling interval: 10 seconds
  • Deduplication window: 2 seconds
  • Revalidates on tab focus

Coordinate Systems

The app internally uses [lat, lng] order. GeoJSON files follow the standard [lng, lat] convention. Conversion happens at the boundary — when loading polylines and when passing data to MapLibre.

Request Flow Example

When a user selects route "30":

  1. routeMap.json resolves "30"["WJB251000068", "WJB251000376", ...]
  2. SWR fetches /api/bus/{routeId} for each route ID → server checks Redis → on miss, calls the public API
  3. Polyline GeoJSON files are loaded for each route ID, split at turn_idx into up and down segments
  4. Bus GPS positions are snapped to the nearest point on the polyline
  5. BusAnimatedMarker smoothly animates markers along the polyline path (3-seconds duration)
  6. Every 10 seconds, SWR refetches and markers animate to updated positions

Client-Side State

Mechanism Scope Data
SWR Live data Bus locations, arrival info (10s polling)
CacheManager Static data Polylines, route/station maps (in-memory LRU)
AppMapContext UI state Global MapRef instance for cross-component map control
localStorage Persistence Map view state (center, zoom, bearing), selected route
useMemo Derived state Active route detection, sorted bus lists, snapped positions

Tech Stack

Category Technology
Framework Next.js 16 (App Router, Turbopack)
UI React 19
Language TypeScript 5
Styling Tailwind CSS 4
Map Renderer MapLibre GL JS via react-map-gl
Live Data SWR for stale-while-revalidate polling
Server Cache Redis for API response caching
Static CDN Vercel Blob for production assets
Linting ESLint 9

Getting Started

  1. Navigate into the Vision directory:

    cd Vision
  2. Install dependencies:

    npm install
  3. Set up environment variables:

    cp .env.local.example .env.local

    Edit .env.local with your credentials:

    # Static data — use local files during development
    NEXT_PUBLIC_USE_REMOTE_STATIC_DATA="false"
    NEXT_PUBLIC_STATIC_API_URL="/data"
    
    # Korea Public Data Portal API key (required for live bus data)
    DATA_GO_KR_SERVICE_KEY="your-service-key"
    
    # Redis URL for server-side caching
    REDIS_URL="redis://..."
    
    # Vercel Blob token (only needed for upload-data script)
    BLOB_READ_WRITE_TOKEN="your-token"
  4. Run the development server:

    npm run dev

    Open http://localhost:3000.

Configuration

All environment variables are centralized in src/shared/config/env.ts with sensible defaults — the app runs without any configuration in development mode (using local static data).

Server-Side

Variable Required Description
DATA_GO_KR_SERVICE_KEY Yes API key for apis.data.go.kr live bus endpoints
BLOB_READ_WRITE_TOKEN No Vercel Blob token (only for upload-data script)
REDIS_URL Yes Redis connection string for API response caching

Client-Side

Variable Default Description
NEXT_PUBLIC_USE_REMOTE_STATIC_DATA false true to load static data from Vercel Blob
NEXT_PUBLIC_STATIC_API_URL /data Base URL for static data (local path or Blob URL)
NEXT_PUBLIC_MAP_DEFAULT_POSITION 37.3421,127.91976 Initial map center (lat, lng)
NEXT_PUBLIC_MAP_DEFAULT_ZOOM 13 Initial zoom level
NEXT_PUBLIC_MAP_MÏAX_BOUNDS 37.10,127.60,37.60,128.30 Map boundary limits
NEXT_PUBLIC_BUS_STOP_MARKER_MIN_ZOOM 15 Zoom threshold for stop markers
NEXT_PUBLIC_BUS_ANIMATION_DURATION 4000 Bus marker animation duration (ms)
NEXT_PUBLIC_LIVE_API_REFRESH_INTERVAL 10000 Live data polling interval (ms)
NEXT_PUBLIC_DEFAULT_ROUTE 30 Default route loaded on first visit

Scripts

Command Description
npm run dev Start dev server with Turbopack
npm run build Production build
npm run start Start production server
npm run lint Run ESLint
npm run lint:fix Run ESLint with auto-fix
npm run typecheck Type-check without emitting
npm run upload-data Upload public/data/ to Vercel Blob