mooner / Resume KO/EN /

mooner:~/resume $ cat projects/hanriv/README.md

지금 한강은。

Real-time Han River temperature

An Android app that shows the Han River’s water temperature. First published on Google Play in 2018 and run as a one-person project, from planning to operations, ever since; in September 2026, V7 rebuilt both the app and the server from scratch.

Google Play ↗

mooner:~/resume/projects/hanriv $ tree -L 1

Structure01

구조

06 parts

V7 is split into the app, a Ktor backend, an admin console and a public backup repository. The app and the server share the same Kotlin models.

Android app
Rewritten in Kotlin with no Java left: temperature, dam and settings tabs, history, notices, three widget sizes, a daily temperature alert and dam discharge alerts. The main screens are in Compose; the shell that hosts the tabs is still ViewPager2 and Fragments.
backend/
The Ktor server: hourly collection and storage, the public API (current values, history, dam discharge), maintenance mode and notices.
shared/
Kotlin Multiplatform models. The app pulls in the backend repository as a submodule and reads both server responses and the backup JSON with them.
git-updater/
A separate process that commits each collection to a public GitHub repository, shipped as a Kotlin/Native binary.
adminpage
A SvelteKit admin console for collection history, errors, data charts, notices and maintenance.
parse-bot
The V5-era collector. It still runs on GitHub Actions eight times a day, writes to Google Sheets, and serves as the app’s last fallback.

mooner:~/resume/projects/hanriv $ cat docs/architecture.md

Architecture02

아키텍처

Data flow

SEOUL OPEN APIwater temperatureWAMISdam dischargeOPENWEATHERweather per sitehourly · cron 0 * * * *ANDROID APPV7 · Google PlayTRAEFIKTLS · /hantemp/v2KTOR BACKENDcollector · API · memoPOSTGRESQLTimescaleDB hypertable1 txREDIS STREAMXADD · XREADGROUPGIT-UPDATERKotlin/NativeGITHUBfallbackfallback read when the API fails// green: what the app reads// dashed: hourly jobs and the backup copy

On the hour, the backend pulls from three public APIs and stores the result in one transaction. The app reads it through Traefik, and when the server does not answer it reads the copy left on GitHub.

  • Each collection is one entry with its temperature and dam rows attached, kept in a TimescaleDB hypertable in 30-day chunks.
  • The current-value API answers from a 30-second in-process cache, cleared the moment a new collection lands.
  • Responses report "not the latest collection" and "observation is old" separately, and age is measured from the observation time, not the fetch time.
  • The app reads API → GitHub copy → old sheet in that order and marks screens that use the copy as backup data. It tries the copy first even during maintenance (503).
  • The backup path is kept off the request path: the server only appends to a Redis Stream, and git-updater handles commits and retries.
  • The upstream API has changed many times: the site moved, station order changed, fields were renamed (W_TEMP → WATT). The backend finds stations by name, and since October 2025 it has accepted both the old and new field names, so the January 2026 rename needed no backend change.

mooner:~/resume/projects/hanriv $ git log --grep=fix

Problem solving03

문제 해결

05 cases

  1. 01

    From an app that read a spreadsheet to a server that collects

    Problem
    Up to V5, a bot fetched the temperature every three hours into a Google Sheets cell, and the app sliced the sheet’s response as a string. History lived on the device, one reading a day, fourteen at most.
    Approach
    A new Ktor backend collects on the hour and stores each collection as one entry in PostgreSQL (TimescaleDB). It serves a history API and gathers dam discharge and weather alongside.
    Result
    With V7, history lives on the server instead of the phone, and the app gained daily charts, period statistics, site comparison and dam discharge alerts.
  2. 02

    An app that opens when the server is down

    Problem
    V5 had one place to read from, the sheet; if it failed, the app showed a connection error and closed. A new server alone would fail the same way whenever it or its database stopped.
    Approach
    After each collection the server appends to a Redis Stream and a separate process, git-updater, commits a snapshot to a public GitHub repository. The app reads that copy with the same shared models as the server, then the old sheet if that fails too. It tries the copy first even during maintenance, and ignores maintenance notices whose window has passed.
    Result
    Hourly snapshots since March 2026 add up to more than 9,000 commits. Reading the copy, marking backup data and checking maintenance windows are pinned by 29 tests.
  3. 03

    The "latest" value was two days old

    Problem
    The current-value API once served a two-day-old temperature as the latest. Temperature and dam data shared a single "latest entry" pointer in Redis, and when the dam API failed, the fallback logic moved that pointer back to an old entry.
    Approach
    The shared pointer is gone: each domain now looks up "the most recent entry that has its rows" inside one read transaction, and "not the latest collection" and "observation is old" became separate fields.
    Result
    A regression test replays the production order (dam, then temperature), and resolving the current value is pinned to three SQL statements. The new fields are omitted at their defaults, so existing app versions keep working.
  4. 04

    The daily alert stopped after a day or two

    Problem
    The daily temperature alert stopped arriving after a day or two. V5 set it as one repeating alarm (setRepeating), and the receiver written to re-arm it after a reboot was never registered in the manifest.
    Approach
    Now a one-shot alarm is set, and when it fires the next one is booked first. Alarms are re-armed on boot, app update and app start, and the receiver fetches the temperature asynchronously with split timeouts so there is always time left to post the notification.
    Result
    Fixed in V7, and the same structure now drives the new dam discharge alerts.
  5. 05

    The right edge of the screen ignored taps

    Problem
    The tab transition only made the neighbouring page transparent, so an invisible page covered the right fifth of the screen and swallowed taps; transparency does not affect hit testing.
    Approach
    The transition maths moved into a pure function, and pages settled off-screen now reset their transform. A test pins the rule that an off-screen page never overlaps the visible one.
    Result
    Eight tests guard against regressions, and V7 fixed the dead strip on the temperature screen.

mooner:~/resume/projects/hanriv $ ls ./screens

Screens04

화면

04 images

V7 home screen in the night theme: Tancheon at 25.9° and a seven-day chart
Home · night theme
The same home screen in the dusk theme
Home · dusk theme
Dam discharge screen with the total and cards for Paldang and Cheongpyeong
Dam discharge
Settings: automatic theme, theme choice and experimental features
Settings

Accepting requestsAnsan --:-- KST

mooner:~/resume $ ./contact.sh

Contact→

Name
문민기 / Minki Moon
Handle
@mooner
Base
Ansan, KR

Happy to trade coffee for conversation.

If anything here catches your eye, feel free to reach out.

// ping me anytime — i read all.

© 2026 Minki Mooncrafted with monospace affection · rev 2026.10.18