Skill: chloepark85/airkorea-cli (version 0.1.0)

--- name: airkorea-cli description: Korean real-time air quality (PM10, PM2.5, O3, NO2, CO, SO2) and 1–3 day forecasts via 한국환경공단 에어코리아 OpenAPI (apis.data.go.kr/B552584). Six subcommands wrap getMsrstnAcctoRltmMesureDnsty (측정소별 실시간), getCtprvnRltmMesureDnsty (시도별 실시간), getMinuDustFrcstDspth (예보), getMsrstnList (측정소 목록), getTMStdrCrdnt (행정동→TM좌표), and getNearbyMsrstnList (TM→근접측정소). Use when building air-quality dashboards, mask/runner notifications, school outdoor-activity advisories, location-based AQI lookups by 행정동 / 시도 / 좌표, or grounding LLM answers about Korean air quality in regulator-curated data. Pairs with juso-address-cli (resolve addresses), kakao-local-cli (WGS84 coords), kr-holiday-cli (calendar overlay), naver-papago-translate (localize for non-KR users), and tistory-api-cli/velog-cli (publish AQI digests). Free data.go.kr tier (1,000 req/day dev, higher on prod approval). version: 0.1.0 license: MIT --- # airkorea-cli Command-line wrapper for **한국환경공단 에어코리아 OpenAPI** — Korea's first-party air-quality system. Operated by the Korea Environment Corporation, it is the data behind airkorea.or.kr, every weather-app AQI badge in Korea, and the 미세먼지 alert that pings 50M phones on bad-haze days. Six subcommands wrap two services: | Command | Endpoint | Purpose | |---|---|---| | `scripts/realtime.sh` | [REDACTED_SECRET] | 측정소별 실시간/일별/월별 측정값 (1h–90d). | | `scripts/sido.sh` | [REDACTED_SECRET] | 시도별 실시간 측정값 (every station in a province). | | `scripts/forecast.sh` | [REDACTED_SECRET] | 1~3일 미세먼지/오존 예보. | | `scripts/station.sh` | [REDACTED_SECRET] | 측정소 directory (filter by sido / addr keyword). | | `scripts/tm.sh` | [REDACTED_SECRET] | 행정/법정동 → TM(중부원점) 좌표. | | `scripts/nearby.sh` | [REDACTED_SECRET] | TM 좌표 → 가까운 측정소 목록 (정렬: 거리). | All output is JSONL — one row per record — so it pipes directly into `jq`, `csvkit`, `pandas`, or downstream skills. ## When to use this skill - **AQI dashboards** — `sido --sido 전국 --num 600` to get a country-wide snapshot in one call; cache hourly. - **Location-based mask/runner alerts** — `tm --umd <my-동>` → `nearby --tm-x … --tm-y …` → `realtime --station <closest>` → push notification when `pm25Grade ≥ 3`. - **School outdoor-activity advisories** — schedule `forecast --date $(date +%F)` daily; surface `informGrade` and `informCause` to staff/parents. - **Korean-news AQI grounding** — when an LLM answers "오늘 강남 미세먼지 어때?", cite real-time station data, not hallucinated numbers. - **Backfill / research panels** — `realtime --period 3MONTH` to pull the last 90 days for any single station; combine with `kr-holiday-cli` to test holiday-traffic effects. ## Do **not** use this skill for - **Sub-hourly or sensor-grade data** — AirKorea publishes at hourly granularity (with a ~1h delay). For minute-level, use IoT vendors (Awair, IQAir partnerships). - **Outside-Korea AQI** — only 한국환경공단 stations. For other countries, use OpenAQ, AirNow, or WAQI. - **Health-effect interpretations** — the API returns concentrations + grades, not personalized health advice. Pair with WHO/CDC guidance text on the consumer side. - **High-throughput public-facing apps without a prod-tier key** — dev-tier ceiling is 1,000 req/day per service. Apply for production tier on data.go.kr. ## Prerequisites 1. **Register at <https://www.data.go.kr/>** (Korean OpenData portal, free, no business required). 2. Apply for **both** services (auto-approved for the dev tier): - "한국환경공단_에어코리아_대기오염정보" (real-time + forecast) - "한국환경공단_에어코리아_측정소정보" (station directory + TM utilities) 3. Copy the **Decoding** key (the raw form, *not* URL-encoded) and export: ```bash export AIRKOREA_SERVICE_KEY='your_decoded_key_here' ``` 4. Optional overrides (rarely needed): ```bash export ARPLTN_BASE='https://apis.data.go.kr/B552584/ArpltnInforInqireSvc' export MSRSTN_BASE='https://apis.data.go.kr/B552584/MsrstnInfoInqireSvc' ``` ## Common workflows ### A) Snapshot every Seoul station right now ```bash scripts/sido.sh --sido 서울 --num 60 \ | jq -c '{stationName, dataTime, pm10Value, pm25Value, pm10Grade, pm25Grade}' ``` ### B) Find my closest station from a 동 name (full chain) ```bash read TMX TMY < <(scripts/tm.sh --umd 역삼동 \ | jq -r 'select(.sidoName=="서울") | "\(.tmX) \(.tmY)"' | head -1) scripts/nearby.sh --tm-x "$TMX" --tm-y "$TMY" | head -3 # → e.g. "강남구", distance ~1.2km scripts/realtime.sh --station 강남구 --period HOUR --num 1 \ | jq -c '{dataTime, pm25Value, pm25Grade, pm10Value, pm10Grade, o3Value, no2Value}' ``` ### C) Daily forecast headline for a digest email ```bash scripts/forecast.sh --date "$(date +%F)" --code PM25 \ | jq -r '"\(.informData) (\(.informCode)) — \(.informGrade // "-")"' ``` ### D) 90-day station history → CSV ```bash scripts/realtime.sh --station 종로구 --period 3MONTH --num 1000 \ | jq -r '[.dataTime, .pm10Value, .pm25Value, .o3Value, .no2Value] | @csv' \ > jongno_90d.csv ``` ### E) Run a "very-bad" alert across the country ```bash scripts/sido.sh --sido 전국 --num 1000 \ | jq -c 'select((.pm25Grade // "-") == "4")' # emit one row per station that's in 매우나쁨 right now → fan out to your alerter ``` ## Output shape — sample (realtime.sh) ```json { "dataTime": "2026-04-29 09:00", "stationName": "강남구", "mangName": "도시대기", "so2Value": "0.003", "coValue": "0.4", "o3Value": "0.041", "no2Value": "0.018", "pm10Value": "47", "pm25Value": "22", "khaiValue": "78", "khaiGrade": "2", "so2Grade": "1", "coGrade": "1", "o3Grade": "1", "no2Grade": "1", "pm10Grade": "1", "pm25Grade": "2" } ``` Grade encoding (used by every `*Grade` field): | Grade | 등급 | PM2.5 µg/m³ | PM10 µg/m³ | |---|---|---|---| | 1 | 좋음 | 0–15 | 0–30 | | 2 | 보통 | 16–35 | 31–80 | | 3 | 나쁨 | 36–75 | 81–150 | | 4 | 매우나쁨 | 76+ | 151+ | ## Errors | AirKorea resultCode | Meaning | This CLI | |---|---|---| | `00` | OK | success, JSONL emitted | | `99` / XML body | 등록되지 않은 키 / Decoding key 미사용 | exit 22 | | `01` | Application error | exit 22 | | `04` | HTTP error | exit 22 | | `30` | 서비스 키 권한 없음 | exit 22 | | `12` | DEPRECATED OpenAPI | exit 22 | | `33` | 일일 트래픽 초과 | exit 22 | If you get an XML payload with `<errMsg>SERVICE_KEY_IS_NOT_REGISTERED_ERROR</errMsg>`, you most likely pasted the Encoded key — switch to the **Decoding** version on data.go.kr. ## Composability with other skills in this hub | Pair with | Why | |---|---| | `juso-address-cli` | Resolve a Korean street address → 행정동 → feed into `tm.sh`. | | `kakao-local-cli` | Convert WGS84 lat/lng (from a map tap) to TM via Kakao geocoding, then `nearby.sh`. | | `kr-holiday-cli` | Overlay holiday weekends on AQI charts (commute drops on holidays — visible in NO2). | | `naver-papago-translate` | Localize forecast strings (`informCause`) for non-KR-speaking users. | | `tistory-api-cli` / `velog-cli` | Publish a daily AQI digest as a blog post. | | `kosis-cli` | Cross-reference long-run AQI trends with population / industrial-emission stats. | ## License MIT — see [LICENSE](./LICENSE). AirKorea data is © 한국환경공단; redistribution is permitted under the data.go.kr open-data terms (https://www.data.go.kr/ugs/selectPortalPolicyView.do).

path: README.md
content: # airkorea-cli

Bash CLI wrapping **한국환경공단 에어코리아 OpenAPI** (apis.data.go.kr/B552584) — Korea's first-party real-time air-quality and forecast system.

Six subcommands cover **측정소별 실시간 · 시도별 실시간 · 예보 · 측정소 디렉토리 · 행정동→TM 좌표 변환 · 근접측정소**. Output is JSONL so it pipes straight into `jq`, `csvkit`, `pandas`, or any downstream skill.

See [SKILL.md](./SKILL.md) for the full reference, workflows, and composability tips.

## Quickstart

```bash
# 1. Register at https://www.data.go.kr/ and approve both:
#    - 한국환경공단_에어코리아_대기오염정보
#    - 한국환경공단_에어코리아_측정소정보
export AIRKOREA_SERVICE_KEY='your_decoded_key_here'

# 2. Snapshot every Seoul station right now.
./scripts/sido.sh --sido 서울 --num 60 \
  | jq -c '{stationName, dataTime, pm25Value, pm25Grade}'

# 3. From a 동 name → closest station → latest reading.
read TMX TMY < <(./scripts/tm.sh --umd 역삼동 | jq -r '.tmX, .tmY' | head -2 | xargs)
./scripts/nearby.sh --tm-x "$TMX" --tm-y "$TMY" | head -3
```

## Requirements

- `curl`, `jq`
- An AirKorea service key (free, 1,000 req/day dev tier)

## License

MIT


path: examples/nearest_station.sh
content: #!/usr/bin/env bash
# Given a 행정동 name (default: 역삼동), find the nearest measuring station
# and emit its latest hourly reading.
#
# Requires: AIRKOREA_SERVICE_KEY exported.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL="$(cd "$here/.." && pwd)"

UMD="${1:-역삼동}"

# Take the first matching umdName in 서울 (you may want to broaden / disambiguate).
read TMX TMY < <("$SKILL/scripts/tm.sh" --umd "$UMD" \
  | jq -r 'select(.sidoName=="서울") | "\(.tmX) \(.tmY)"' | head -1)

if [[ -z "${TMX:-}" || -z "${TMY:-}" ]]; then
  echo "no Seoul match for umdName '$UMD'." >&2
  exit 64
fi

STN=$("$SKILL/scripts/nearby.sh" --tm-x "$TMX" --tm-y "$TMY" \
  | jq -r '.stationName' | head -1)

[[ -z "${STN:-}" ]] && { echo "no nearby station found near TM=($TMX,$TMY)." >&2; exit 22; }

"$SKILL/scripts/realtime.sh" --station "$STN" --period HOUR --num 1 \
  | jq -c --arg umd "$UMD" --arg stn "$STN" \
      '{umdName: $umd, station: $stn, dataTime, pm25Value, pm25Grade, pm10Value, pm10Grade, o3Value, no2Value}'


path: examples/seoul_pm25_alert.sh
content: #!/usr/bin/env bash
# Emit one row per Seoul station whose PM2.5 grade is currently 나쁨(3) or 매우나쁨(4).
# Use as the body of a runner/parent/employee push-notification job.
#
# Requires: AIRKOREA_SERVICE_KEY exported.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL="$(cd "$here/.." && pwd)"

"$SKILL/scripts/sido.sh" --sido 서울 --num 60 \
  | jq -c 'select((.pm25Grade // "-") | tonumber? // 0 | . >= 3)
           | {stationName, dataTime, pm25Value, pm25Grade, pm10Value}'


path: scripts/_common.sh
content: #!/usr/bin/env bash
# _common.sh — shared helpers for airkorea-cli.
# Source this from each subcommand: `. "$here/_common.sh"`
#
# Wraps two 한국환경공단 (Korea Environment Corp.) services on data.go.kr:
#   - ArpltnInforInqireSvc: real-time air-quality measurements + forecasts.
#   - MsrstnInfoInqireSvc:  measuring-station directory + TM-coordinate utils.

set -euo pipefail

ARPLTN_BASE="${ARPLTN_BASE:-https://apis.data.go.kr/B552584/ArpltnInforInqireSvc}"
MSRSTN_BASE="${MSRSTN_BASE:-https://apis.data.go.kr/B552584/MsrstnInfoInqireSvc}"

require_bin() {
  for b in "$@"; do
    command -v "$b" >/dev/null 2>&1 || { echo "error: $b is required but not installed." >&2; exit 127; }
  done
}

require_key() {
  if [[ -z "${AIRKOREA_SERVICE_KEY:-}" ]]; then
    cat >&2 <<'EOF'
error: AIRKOREA_SERVICE_KEY must be set.
Register at https://www.data.go.kr/ and request approval (instant) for:
  "한국환경공단_에어코리아_대기오염정보"        (real-time + forecast)
  "한국환경공단_에어코리아_측정소정보"          (station directory + TM utilities)
Then export the *Decoding* key (raw, not URL-encoded) as AIRKOREA_SERVICE_KEY.
EOF
    exit 78
  fi
}

# valid_sido <name>  — accept either Korean or English label.
valid_sido() {
  case "$1" in
    전국|서울|부산|대구|인천|광주|대전|울산|경기|강원|충북|충남|전북|전남|경북|경남|제주|세종) ;;
    *) echo "error: sidoName '$1' invalid. Use one of: 전국 서울 부산 대구 인천 광주 대전 울산 경기 강원 충북 충남 전북 전남 경북 경남 제주 세종." >&2; exit 64;;
  esac
}

# valid_yyyymmdd <YYYY-MM-DD>
valid_yyyy_mm_dd() {
  if ! [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
    echo "error: date '$1' must be YYYY-MM-DD." >&2
    exit 64
  fi
}

# valid_period <DAILY|HOUR|MONTH|3MONTH>
valid_period() {
  case "$1" in
    DAILY|HOUR|MONTH|3MONTH) ;;
    *) echo "error: dataTerm '$1' must be one of: DAILY HOUR MONTH 3MONTH." >&2; exit 64;;
  esac
}

# airkorea_get <base> <path> <key1=val1> ...
# - Adds serviceKey, returnType=json, _returnType=json, ver=1.5 automatically.
# - URL-encodes values via jq -r @uri.
# - Returns body on stdout. Non-2xx or non-OK resultCode prints to stderr and exits 22.
airkorea_get() {
  local base="$1" path="$2"; shift 2
  require_key

  local enc_key
  enc_key=$(printf '%s' "$AIRKOREA_SERVICE_KEY" | jq -Rrn '@uri inputs')
  # ver=1.5 surfaces extra metric flags (so2Flag, coFlag, …) on the realtime endpoints; harmless on others.
  local qs="serviceKey=${enc_key}&returnType=json&_returnType=json&ver=1.5"

  local kv k v enc_v
  for kv in "$@"; do
    [[ -z "$kv" ]] && continue
    k="${kv%%=*}"
    v="${kv#*=}"
    [[ -z "$v" ]] && continue
    enc_v=$(printf '%s' "$v" | jq -Rrn '@uri inputs')
    qs="${qs}&${k}=${enc_v}"
  done

  local out http
  out=$(mktemp)
  http=$(curl -sS -G -o "$out" -w '%{http_code}' "${base}/${path}?${qs}" || true)

  if [[ "$http" != 2* ]]; then
    echo "error: AirKorea returned HTTP $http for /${path}" >&2
    sed -e 's/^/  /' "$out" >&2
    rm -f "$out"
    exit 22
  fi

  # data.go.kr sometimes returns XML (with a SERVICE_KEY error envelope) even when returnType=json.
  if head -c 1 "$out" | grep -q '<'; then
    echo "error: AirKorea returned XML (likely a service-key or quota error) for /${path}" >&2
    sed -e 's/^/  /' "$out" >&2
    rm -f "$out"
    exit 22
  fi

  # Verify the JSON envelope's resultCode == "00".
  local rc rmsg
  rc=$(jq -r '.response.header.resultCode // "?"' "$out")
  rmsg=$(jq -r '.response.header.resultMsg  // ""'  "$out")
  if [[ "$rc" != "00" ]]; then
    echo "error: AirKorea resultCode=$rc resultMsg=$rmsg for /${path}" >&2
    rm -f "$out"
    exit 22
  fi

  cat "$out"
  rm -f "$out"
}

# emit_items <body-json>
# Extracts response.body.items as JSONL. Handles both array and single-object shapes.
emit_items() {
  jq -c '
    .response.body.items as $it
    | if   $it == null then empty
      elif ($it | type) == "array"  then $it[]
      else $it
      end
  '
}


path: scripts/forecast.sh
content: #!/usr/bin/env bash
# forecast.sh — getMinuDustFrcstDspth: 대기오염 예보통보 조회.
#
# Returns the most-recent forecast announcements for a search date. Each row
# carries `informCode` (PM10|PM25|O3), `informGrade` (region-by-region grades:
# "서울 : 보통,제주 : 좋음,..."), `informData` (forecast target date),
# `informCause` (날씨/기류 설명), `dataTime` (announcement time).
#
# Usage:
#   forecast.sh --date 2026-04-29
#   forecast.sh --date 2026-04-29 --code PM25
#
# code: PM10  PM25  O3   (default: all three returned)
#
# Output: JSONL — one row per (informCode × dataTime).
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$here/_common.sh"

date_in="" code="" num="20" page="1"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --date)  date_in="$2"; shift 2;;
    --code)  code="$2";    shift 2;;
    --num)   num="$2";     shift 2;;
    --page)  page="$2";    shift 2;;
    -h|--help)
      sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
      exit 0;;
    *) echo "error: unknown flag '$1'" >&2; exit 64;;
  esac
done

[[ -z "$date_in" ]] && { echo "error: --date is required (YYYY-MM-DD)." >&2; exit 64; }
valid_yyyy_mm_dd "$date_in"
if [[ -n "$code" ]]; then
  case "$code" in PM10|PM25|O3) ;; *) echo "error: --code must be one of: PM10 PM25 O3." >&2; exit 64;; esac
fi

require_bin curl jq

resp=$(airkorea_get "$ARPLTN_BASE" "getMinuDustFrcstDspth" \
  "searchDate=$date_in" "InformCode=$code" \
  "numOfRows=$num" "pageNo=$page")
echo "$resp" | emit_items


path: scripts/nearby.sh
content: #!/usr/bin/env bash
# nearby.sh — getNearbyMsrstnList: 근접측정소 목록 조회.
#
# Given TM coordinates (X, Y) — usually fetched via tm.sh first — return the
# closest measuring stations sorted by distance.
#
# Usage:
#   nearby.sh --tm-x 244148.546388 --tm-y 412423.75772
#
# Tip: chain with tm.sh —
#   eval $(scripts/tm.sh --umd 역삼동 \
#     | jq -r 'select(.sidoName=="서울") | "TMX=\(.tmX); TMY=\(.tmY)"' | head -1)
#   scripts/nearby.sh --tm-x "$TMX" --tm-y "$TMY"
#
# Output: JSONL — one row per nearby station (stationName, addr, tm).
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$here/_common.sh"

tm_x="" tm_y=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --tm-x)  tm_x="$2"; shift 2;;
    --tm-y)  tm_y="$2"; shift 2;;
    -h|--help)
      sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
      exit 0;;
    *) echo "error: unknown flag '$1'" >&2; exit 64;;
  esac
done

[[ -z "$tm_x" || -z "$tm_y" ]] && { echo "error: --tm-x and --tm-y are both required (use tm.sh to resolve)." >&2; exit 64; }
require_bin curl jq

resp=$(airkorea_get "$MSRSTN_BASE" "getNearbyMsrstnList" \
  "tmX=$tm_x" "tmY=$tm_y")
echo "$resp" | emit_items


path: scripts/realtime.sh
content: #!/usr/bin/env bash
# realtime.sh — getMsrstnAcctoRltmMesureDnsty: 측정소별 실시간 측정값 조회.
#
# Returns one row per hour for the named station, with PM10 / PM2.5 / O3 / NO2
# / CO / SO2 concentrations + grade flags (1=좋음 2=보통 3=나쁨 4=매우나쁨).
#
# Usage:
#   realtime.sh --station 종로구
#   realtime.sh --station 강남구 --period DAILY --num 24
#
# period (dataTerm): DAILY=24h HOUR=1h MONTH=30d 3MONTH=90d
#
# Output: JSONL — one row per measurement timestamp.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$here/_common.sh"

station="" period="DAILY" num="100" page="1"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --station)  station="$2"; shift 2;;
    --period)   period="$2";  shift 2;;
    --num)      num="$2";     shift 2;;
    --page)     page="$2";    shift 2;;
    -h|--help)
      sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'
      exit 0;;
    *) echo "error: unknown flag '$1'" >&2; exit 64;;
  esac
done

[[ -z "$station" ]] && { echo "error: --station is required (e.g. 종로구, 강남구; use station.sh to look up)." >&2; exit 64; }
valid_period "$period"
require_bin curl jq

resp=$(airkorea_get "$ARPLTN_BASE" "getMsrstnAcctoRltmMesureDnsty" \
  "stationName=$station" "dataTerm=$period" \
  "numOfRows=$num" "pageNo=$page")
echo "$resp" | emit_items


path: scripts/sido.sh
content: #!/usr/bin/env bash
# sido.sh — getCtprvnRltmMesureDnsty: 시도별 실시간 측정정보 조회.
#
# Returns one row per measuring station within a given province, with current
# PM10 / PM2.5 / O3 / NO2 / CO / SO2 readings + grade flags.
#
# Usage:
#   sido.sh --sido 서울
#   sido.sh --sido 전국 --num 600
#
# sido: 전국 서울 부산 대구 인천 광주 대전 울산 경기 강원 충북 충남 전북 전남 경북 경남 제주 세종
#
# Output: JSONL — one row per station.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$here/_common.sh"

sido="" num="100" page="1"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --sido)  sido="$2"; shift 2;;
    --num)   num="$2";  shift 2;;
    --page)  page="$2"; shift 2;;
    -h|--help)
      sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'
      exit 0;;
    *) echo "error: unknown flag '$1'" >&2; exit 64;;
  esac
done

[[ -z "$sido" ]] && { echo "error: --sido is required." >&2; exit 64; }
valid_sido "$sido"
require_bin curl jq

resp=$(airkorea_get "$ARPLTN_BASE" "getCtprvnRltmMesureDnsty" \
  "sidoName=$sido" "numOfRows=$num" "pageNo=$page")
echo "$resp" | emit_items


path: scripts/station.sh
content: #!/usr/bin/env bash
# station.sh — getMsrstnList: 측정소 목록 조회.
#
# Search the directory of all 도시대기/도로변대기/국가배경/교외대기/항만 측정소.
# Filter by sido name and/or address keyword (works as a contains-match on
# `addr` — e.g. "강남구", "수원시 영통구").
#
# Usage:
#   station.sh --keyword 강남
#   station.sh --sido 서울 --num 200
#
# Output: JSONL — one row per station (stationName, addr, dmX, dmY, mangName, year).
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$here/_common.sh"

sido="" keyword="" num="100" page="1"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --sido)     sido="$2";    shift 2;;
    --keyword)  keyword="$2"; shift 2;;
    --num)      num="$2";     shift 2;;
    --page)     page="$2";    shift 2;;
    -h|--help)
      sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
      exit 0;;
    *) echo "error: unknown flag '$1'" >&2; exit 64;;
  esac
done

[[ -n "$sido" ]] && valid_sido "$sido"
require_bin curl jq

resp=$(airkorea_get "$MSRSTN_BASE" "getMsrstnList" \
  "addr=$keyword" "stationName=" \
  "numOfRows=$num" "pageNo=$page")

# Optional client-side filter by sido. data.go.kr's getMsrstnList does not have
# a sido parameter; it's exposed only through `addr` keyword. So when --sido is
# given, we filter the JSONL stream.
if [[ -n "$sido" ]]; then
  echo "$resp" | emit_items | jq -c --arg s "$sido" 'select((.addr // "") | startswith($s))'
else
  echo "$resp" | emit_items
fi


path: scripts/tm.sh
content: #!/usr/bin/env bash
# tm.sh — getTMStdrCrdnt: TM 기준좌표 조회.
#
# Resolve a 행정동/법정동 name to its TM(중부원점) X/Y coordinates — needed by
# nearby.sh (getNearbyMsrstnList only accepts TM coordinates, not WGS84).
#
# Usage:
#   tm.sh --umd 역삼동
#   tm.sh --umd 양재동 --num 5
#
# Output: JSONL — one row per matching umdName (umdName, sggName, sidoName, tmX, tmY).
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$here/_common.sh"

umd="" num="100" page="1"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --umd)   umd="$2";  shift 2;;
    --num)   num="$2";  shift 2;;
    --page)  page="$2"; shift 2;;
    -h|--help)
      sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
      exit 0;;
    *) echo "error: unknown flag '$1'" >&2; exit 64;;
  esac
done

[[ -z "$umd" ]] && { echo "error: --umd is required (행정/법정동 name, e.g. '역삼동')." >&2; exit 64; }
require_bin curl jq

resp=$(airkorea_get "$MSRSTN_BASE" "getTMStdrCrdnt" \
  "umdName=$umd" "numOfRows=$num" "pageNo=$page")
echo "$resp" | emit_items


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)