Skill: steamb23/kma-weather (version 1.0.3)

--- name: kma-weather description: Get weather from Korea Meteorological Administration (기상청). Provides current conditions, forecasts (3-10 days), and weather warnings/advisories (기상특보). Use when user needs Korean weather data, 기상특보, or precise local forecasts (5km grid). Requires KMA_SERVICE_KEY. metadata: {"openclaw":{"emoji":"🌦️","homepage":"https://www.data.go.kr/data/15084084/openapi.do","requires":{"bins":["python3"],"env":["KMA_SERVICE_KEY"]},"primaryEnv":"KMA_SERVICE_KEY"}} --- # kma-weather ## Quick Start ```bash # Current weather + 6-hour forecast python3 skills/kma-weather/scripts/forecast.py brief --lat 37.5665 --lon 126.9780 # All forecasts as JSON (current + ultrashort + shortterm) python3 skills/kma-weather/scripts/forecast.py all --lat 37.5665 --lon 126.9780 --json # Short-term forecast (3 days) python3 skills/kma-weather/scripts/forecast.py shortterm --lat 37.5665 --lon 126.9780 --days all # Nationwide weather warnings/advisories (기상특보) python3 skills/kma-weather/scripts/weather_warnings.py # Mid-term forecast (3-10 days) python3 skills/kma-weather/scripts/midterm.py --region 서울 ``` ## Setup ### 1. Get API Key 1. Visit [공공데이터포털](https://www.data.go.kr) 2. Request access to these 3 APIs (all use the same key): - [기상청 단기예보 조회서비스](https://www.data.go.kr/data/15084084/openapi.do) (15084084) - [기상청 기상특보 조회서비스](https://www.data.go.kr/data/15000415/openapi.do) (15000415) - [기상청 중기예보 조회서비스](https://www.data.go.kr/data/15059468/openapi.do) (15059468) 3. Copy your `ServiceKey` from My Page → API Key Management ### 2. Set Environment Variable In `~/.openclaw/openclaw.json`: **Sandbox** (add to `agents.defaults.sandbox.docker.env`): ```json { "agents": { "defaults": { "sandbox": { "docker": { "env": { "KMA_SERVICE_KEY": "your-key" } } } } } } ``` **Host** (add to `env.vars`): ```json { "env": { "vars": { "KMA_SERVICE_KEY": "your-key" } } } ``` ## Usage ### forecast.py | Command | Description | |---------|-------------| | `current` | Real-time observations | | `ultrashort` | 6-hour forecast | | `shortterm` | 3-day forecast | | `brief` | current + ultrashort | | `all` | current + ultrashort + shortterm | **Options**: - `--lat`, `--lon`: Coordinates (required) - `--days`: For shortterm - `1` (tomorrow, default), `2`, `3`, or `all` - `--json`: Raw JSON output **Output example** (`current`): ``` 🌤️ 현재 날씨 (초단기실황) 🌡️ 기온: 5.2°C 💧 습도: 65% 🌧️ 강수량: 0mm (1시간) 💨 풍속: 2.3m/s 🧭 풍향: NW (315°) ``` ### weather_warnings.py Returns current nationwide 기상특보: ``` 🚨 기상특보 현황 발표시각: 2026-02-01 10:00 발효시각: 2026-02-01 10:00 📍 현재 발효 중인 특보 • 건조경보 : 강원도, 경상북도, ... • 풍랑주의보 : 동해중부안쪽먼바다, ... ⚠️ 예비특보 • (1) 강풍 예비특보 : 02월 02일 새벽(00시~06시) : 울릉도.독도 ``` ### midterm.py 3-10 day forecast by region. ```bash python3 skills/kma-weather/scripts/midterm.py --region 서울 python3 skills/kma-weather/scripts/midterm.py --stn-id 109 ``` **Regions**: 서울, 인천, 경기, 부산, 대구, 광주, 대전, 울산, 세종, 강원, 충북, 충남, 전북, 전남, 경북, 경남, 제주 ### grid_converter.py Convert lat/lon to KMA 5km grid (auto-handled by other scripts): ```bash python3 skills/kma-weather/scripts/grid_converter.py 37.5665 126.9780 # Output: Grid: (60, 127) ``` ## API Notes - **Release Schedule**: - Current: Every hour at :40 (base_time: HH00) - Ultra-short: Every hour at :45 (base_time: HH30) - Short-term: 02:10, 05:10, 08:10, 11:10, 14:10, 17:10, 20:10, 23:10 (KST) - Mid-term: 06:00, 18:00 (KST) - **Coverage**: South Korea only - **Auto-pagination**: Scripts fetch all pages automatically ## vs weather skill | | weather | kma-weather | |-|---------|-------------| | Coverage | Global | Korea only | | API Key | No | **Required** | | Resolution | City-level | 5km grid | | Weather Warnings | No | **Yes** (기상특보) | **Use both**: `weather` for global, `kma-weather` for detailed Korean forecasts and 기상특보. ## Troubleshooting | Error | Solution | |-------|----------| | `KMA API service key not found` | Set `KMA_SERVICE_KEY` env var | | `SERVICE_KEY_IS_NOT_REGISTERED_ERROR` | Check API approval status, verify key | | `SERVICE_TIMEOUT_ERROR` | Retry later | | No data returned | Verify coordinates are in South Korea | ## References (Raw API Documentation) - [references/api-forecast.md](references/api-forecast.md) - 단기예보 API endpoints, parameters, response format - [references/api-warnings.md](references/api-warnings.md) - 기상특보 API endpoints, parameters, response format - [references/api-midterm.md](references/api-midterm.md) - 중기예보 API endpoints, parameters, response format - [references/category-codes.md](references/category-codes.md) - KMA category codes (SKY, PTY, etc.) - [implement-status.md](implement-status.md) - Implementation status

path: implement-status.md
content: # Implemented Endpoints

**Short-term Forecast API (VilageFcstInfoService_2.0)**:
- ✅ `/getUltraSrtNcst` - Ultra-short-term observation (현재 날씨)
- ✅ `/getUltraSrtFcst` - Ultra-short-term forecast (6시간 예보)
- ✅ `/getVilageFcst` - Short-term forecast (3일 예보)
- ❌ `/getFcstVersion` - Forecast version query (미구현)

**Weather Warnings API (WthrWrnInfoService)**:
- ✅ `/getPwnStatus` - Warning status summary (특보현황조회)
- ❌ `/getWthrWrnList` - Warning list (기상특보목록조회)
- ❌ `/getWthrWrnMsg` - Warning message (기상특보통보문조회)
- ❌ `/getWthrInfoList` - Weather info list (기상정보목록조회)
- ❌ `/getWthrInfo` - Weather info (기상정보문조회)
- ❌ `/getWthrBrkNewsList` - Breaking news list (기상속보목록조회)
- ❌ `/getWthrBrkNews` - Breaking news (기상속보조회)
- ❌ `/getWthrPwnList` - Preliminary warning list (기상예비특보목록조회)
- ❌ `/getWthrPwn` - Preliminary warning (기상예비특보조회)
- ❌ `/getPwnCd` - Warning code (특보코드조회)

**Mid-term Forecast API (MidFcstInfoService)**:
- ✅ `/getMidFcst` - Mid-term outlook (중기전망조회)
- ❌ `/getMidLandFcst` - Mid-term land forecast (중기육상예보조회, 4-10일 육상날씨)
- ❌ `/getMidTa` - Mid-term temperature (중기기온조회, 4-10일 최저/최고기온)
- ❌ `/getMidSeaFcst` - Mid-term sea forecast (중기해상예보조회, 4-10일 해상날씨/파고)

path: scripts/forecast.py
content: #!/usr/bin/env python3
"""
KMA Short-term Forecast

Retrieves short-term weather forecasts from Korea Meteorological Administration:
- current: Ultra-short-term observation (current weather)
- ultrashort: Ultra-short-term forecast (6 hours)
- shortterm: Short-term forecast (3 days)

Requires KMA_SERVICE_KEY environment variable.
"""

import sys
import json
import argparse
from datetime import datetime, timedelta
from typing import Dict, List, Tuple

# Import local modules
try:
    from grid_converter import latlon_to_grid
    from kma_api import fetch_api, print_error
except ImportError:
    # Try relative import if run as script
    import os
    sys.path.insert(0, os.path.dirname(__file__))
    from grid_converter import latlon_to_grid
    from kma_api import fetch_api, print_error


# API Configuration
BASE_URL = "https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0"
ENDPOINTS = {
    "current": "/getUltraSrtNcst",      # Ultra-short-term observation
    "ultrashort": "/getUltraSrtFcst",   # Ultra-short-term forecast
    "shortterm": "/getVilageFcst",      # Short-term forecast
}


def get_base_time(forecast_type: str) -> Tuple[str, str]:
    """
    Calculate base_date and base_time for API request.

    KMA API release schedules:
    - current: Every hour at :40 (base_time: HH00)
    - ultrashort: Every hour at :45 (base_time: HH30)
    - shortterm: 02:10, 05:10, 08:10, 11:10, 14:10, 17:10, 20:10, 23:10

    Args:
        forecast_type: One of "current", "ultrashort", "shortterm"

    Returns:
        tuple: (base_date, base_time) as strings (YYYYMMDD, HHmm)

    Example:
        >>> get_base_time("current")
        ('20260201', '1400')
    """
    now = datetime.now()

    if forecast_type == "current":
        # Released every hour at :40 (base_time: HH00)
        # If current time is before :40, use previous hour
        if now.minute < 40:
            now = now - timedelta(hours=1)

        base_date = now.strftime("%Y%m%d")
        base_time = now.strftime("%H00")

    elif forecast_type == "ultrashort":
        # Released every hour at :45 (base_time: HH30)
        # If current time is before :45, use previous hour's 30
        if now.minute < 45:
            now = now - timedelta(hours=1)

        base_date = now.strftime("%Y%m%d")
        base_time = now.strftime("%H30")

    elif forecast_type == "shortterm":
        # Released at 02:10, 05:10, 08:10, 11:10, 14:10, 17:10, 20:10, 23:10
        base_times = ["0200", "0500", "0800", "1100", "1400", "1700", "2000", "2300"]

        # Find the most recent base time
        current_hour = now.hour
        current_minute = now.minute

        # Determine which base_time to use
        if current_hour < 2 or (current_hour == 2 and current_minute < 10):
            # Use yesterday's 23:00
            now = now - timedelta(days=1)
            base_time = "2300"
        else:
            # Find the latest base_time before current time
            for i in range(len(base_times) - 1, -1, -1):
                bt_hour = int(base_times[i][:2])
                if current_hour > bt_hour or (current_hour == bt_hour and current_minute >= 10):
                    base_time = base_times[i]
                    break

        base_date = now.strftime("%Y%m%d")

    else:
        raise ValueError(f"Invalid forecast_type: {forecast_type}")

    return base_date, base_time


def fetch_forecast(
    forecast_type: str,
    lat: float,
    lon: float,
    num_rows: int = 300
) -> Dict:
    """
    Fetch weather forecast from KMA API with automatic pagination.

    Args:
        forecast_type: One of "current", "ultrashort", "shortterm"
        lat: Latitude in decimal degrees
        lon: Longitude in decimal degrees
        num_rows: Rows per page (default: 300)

    Returns:
        dict: API response data with all items merged

    Raises:
        Exception: On API errors
    """
    # Convert lat/lon to grid coordinates
    nx, ny = latlon_to_grid(lat, lon)

    # Get base time
    base_date, base_time = get_base_time(forecast_type)

    # Build API URL
    url = f"{BASE_URL}{ENDPOINTS[forecast_type]}"

    params = {
        "numOfRows": str(num_rows),
        "pageNo": "1",
        "dataType": "JSON",
        "base_date": base_date,
        "base_time": base_time,
        "nx": str(nx),
        "ny": str(ny),
    }

    # First request
    data = fetch_api(url, params)

    # Check if pagination needed
    try:
        body = data["response"]["body"]
        total_count = int(body.get("totalCount", 0))
        items = body.get("items", {}).get("item", [])

        # If we got all items, return as-is
        if total_count <= num_rows:
            return data

        # Fetch remaining pages
        all_items = items if isinstance(items, list) else [items]
        total_pages = (total_count + num_rows - 1) // num_rows

        for page in range(2, total_pages + 1):
            params["pageNo"] = str(page)
            page_data = fetch_api(url, params)
            page_items = page_data["response"]["body"]["items"].get("item", [])
            if isinstance(page_items, list):
                all_items.extend(page_items)
            else:
                all_items.append(page_items)

        # Update response with merged items
        data["response"]["body"]["items"]["item"] = all_items
        return data

    except (KeyError, TypeError):
        return data


def format_current(data: Dict, lat: float = None, lon: float = None,
                   nx: int = None, ny: int = None) -> str:
    """
    Format ultra-short-term observation data (current weather).

    Args:
        data: API response data
        lat: Latitude in decimal degrees (optional)
        lon: Longitude in decimal degrees (optional)
        nx: KMA grid X coordinate (optional)
        ny: KMA grid Y coordinate (optional)

    Returns:
        str: Formatted text output
    """
    try:
        items = data["response"]["body"]["items"]["item"]
    except (KeyError, TypeError):
        return "Error: Invalid API response format"

    if not items:
        return "No data available"

    # Parse category values
    values = {}
    for item in items:
        category = item.get("category")
        obsrValue = item.get("obsrValue", "N/A")
        values[category] = obsrValue

    # Build output
    output = []
    output.append("🌤️ 현재 날씨 (초단기실황)")
    location_line = format_location_line(lat, lon, nx, ny)
    if location_line:
        output.append(location_line)

    # Temperature
    if "T1H" in values:
        output.append(f"🌡️  기온: {values['T1H']}°C")

    # Humidity
    if "REH" in values:
        output.append(f"💧 습도: {values['REH']}%")

    # Precipitation (1 hour)
    if "RN1" in values:
        rn1 = values['RN1']
        if rn1 == "0" or rn1 == "강수없음":
            output.append("🌧️  강수량: 0mm (1시간)")
        else:
            output.append(f"🌧️  강수량: {rn1}mm (1시간)")

    # Wind speed
    if "WSD" in values:
        output.append(f"💨 풍속: {values['WSD']}m/s")

    # Wind direction
    if "VEC" in values:
        vec = values['VEC']
        direction = get_wind_direction(vec)
        output.append(f"🧭 풍향: {direction} ({vec}°)")

    # Atmospheric pressure
    if "PTY" in values:
        pty = values['PTY']
        pty_text = get_precipitation_type(pty)
        if pty != "0":
            output.append(f"☔ 강수형태: {pty_text}")

    return "\n".join(output)


def get_wind_direction(degree: str) -> str:
    """
    Convert wind direction in degrees to cardinal direction.

    Args:
        degree: Wind direction in degrees (0-360)

    Returns:
        str: Cardinal direction (N, NE, E, SE, S, SW, W, NW)
    """
    try:
        deg = float(degree)
    except (ValueError, TypeError):
        return "N/A"

    directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
    index = int((deg + 22.5) / 45) % 8
    return directions[index]


def get_precipitation_type(pty_code: str) -> str:
    """
    Convert PTY code to precipitation type text.

    Args:
        pty_code: PTY code (0-4)

    Returns:
        str: Precipitation type
    """
    pty_map = {
        "0": "없음",
        "1": "비",
        "2": "비/눈",
        "3": "눈",
        "4": "소나기",
    }
    return pty_map.get(pty_code, "알 수 없음")


def get_sky_condition(sky_code: str) -> str:
    """
    Convert SKY code to sky condition text.

    Args:
        sky_code: SKY code (1-4)

    Returns:
        str: Sky condition
    """
    sky_map = {
        "1": "맑음",
        "3": "구름많음",
        "4": "흐림",
    }
    return sky_map.get(sky_code, "알 수 없음")


def format_location_line(lat: float = None, lon: float = None,
                         nx: int = None, ny: int = None) -> str:
    """Format location information line for forecast output."""
    if lat is None or lon is None:
        return ""
    parts = [f"📍 위치: {lat}, {lon}"]
    if nx is not None and ny is not None:
        parts.append(f"(격자: {nx}, {ny})")
    return " ".join(parts)


def format_ultrashort(data: Dict, lat: float = None, lon: float = None,
                      nx: int = None, ny: int = None) -> str:
    """
    Format ultra-short-term forecast data (6 hours).

    Args:
        data: API response data
        lat: Latitude in decimal degrees (optional)
        lon: Longitude in decimal degrees (optional)
        nx: KMA grid X coordinate (optional)
        ny: KMA grid Y coordinate (optional)

    Returns:
        str: Formatted text output
    """
    try:
        items = data["response"]["body"]["items"]["item"]
    except (KeyError, TypeError):
        return "Error: Invalid API response format"

    if not items:
        return "No data available"

    # Group by forecast date and time
    forecasts = {}
    for item in items:
        fcst_date = item.get("fcstDate", "")
        fcst_time = item.get("fcstTime", "")
        category = item.get("category")
        fcst_value = item.get("fcstValue", "N/A")

        key = f"{fcst_date}_{fcst_time}"
        if key not in forecasts:
            forecasts[key] = {"date": fcst_date, "time": fcst_time}
        forecasts[key][category] = fcst_value

    # Build output
    output = []
    output.append("⏱️ 초단기예보 (6시간)")
    location_line = format_location_line(lat, lon, nx, ny)
    if location_line:
        output.append(location_line)

    for key in sorted(forecasts.keys())[:6]:  # Show up to 6 hours
        fc = forecasts[key]
        time_str = f"{fc['time'][:2]}:{fc['time'][2:]}"

        output.append(f"\n⏰ {time_str}")

        # Temperature
        if "T1H" in fc:
            output.append(f"  🌡️  {fc['T1H']}°C")

        # Sky condition
        if "SKY" in fc:
            sky_text = get_sky_condition(fc["SKY"])
            output.append(f"  ☁️  {sky_text}")

        # Precipitation type
        if "PTY" in fc and fc["PTY"] != "0":
            pty_text = get_precipitation_type(fc["PTY"])
            output.append(f"  ☔ {pty_text}")

        # Precipitation amount
        if "RN1" in fc:
            rn1 = fc["RN1"]
            if rn1 not in ["0", "강수없음"]:
                output.append(f"  🌧️  강수량: {rn1}mm")

        # Humidity
        if "REH" in fc:
            output.append(f"  💧 습도: {fc['REH']}%")

    return "\n".join(output)


def format_shortterm(data: Dict, days=1, lat: float = None, lon: float = None,
                     nx: int = None, ny: int = None) -> str:
    """
    Format short-term forecast data (3 days).

    Args:
        data: API response data
        days: Days from today ('all'=all days, 1=tomorrow, 2=day after, 3=3 days later)
        lat: Latitude in decimal degrees (optional)
        lon: Longitude in decimal degrees (optional)
        nx: KMA grid X coordinate (optional)
        ny: KMA grid Y coordinate (optional)

    Returns:
        str: Formatted text output
    """
    try:
        items = data["response"]["body"]["items"]["item"]
    except (KeyError, TypeError):
        return "Error: Invalid API response format"

    if not items:
        return "No data available"

    # Group by date and time
    forecasts = {}
    for item in items:
        fcst_date = item.get("fcstDate")
        fcst_time = item.get("fcstTime")
        category = item.get("category")
        fcst_value = item.get("fcstValue", "N/A")

        key = f"{fcst_date}_{fcst_time}"
        if key not in forecasts:
            forecasts[key] = {"date": fcst_date, "time": fcst_time}
        forecasts[key][category] = fcst_value

    # Filter by target date(s)
    if days == "all":
        # Show all available dates
        filtered_keys = sorted(forecasts.keys())
        output = []
        output.append("📆 단기예보 (전체)")
        location_line = format_location_line(lat, lon, nx, ny)
        if location_line:
            output.append(location_line)
    else:
        # Calculate target date for filtering
        target_date = (datetime.now() + timedelta(days=int(days))).strftime("%Y%m%d")
        filtered_keys = [k for k in sorted(forecasts.keys()) if forecasts[k]["date"] == target_date]

        day_label = {1: "내일", 2: "모레", 3: "글피"}.get(int(days), f"{days}일 후")
        date_formatted = f"{target_date[:4]}-{target_date[4:6]}-{target_date[6:8]}"
        output = []
        output.append(f"📆 단기예보 ({day_label}, {date_formatted})")
        location_line = format_location_line(lat, lon, nx, ny)
        if location_line:
            output.append(location_line)

    if not filtered_keys:
        output.append("\n해당 날짜의 예보 데이터가 없습니다.")
        return "\n".join(output)

    # Group forecasts by date when showing all days
    if days == "all":
        current_date = None
        for key in filtered_keys:
            fc = forecasts[key]
            date = fc["date"]
            time = fc["time"]

            # Add date header if date changes
            if date != current_date:
                current_date = date
                date_formatted = f"{date[:4]}-{date[4:6]}-{date[6:8]}"
                # Calculate days from now
                date_obj = datetime.strptime(date, "%Y%m%d")
                now_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
                days_diff = (date_obj - now_date).days
                if days_diff == 0:
                    day_label = "오늘"
                elif days_diff == 1:
                    day_label = "내일"
                elif days_diff == 2:
                    day_label = "모레"
                elif days_diff == 3:
                    day_label = "글피"
                else:
                    day_label = f"{days_diff}일 후"
                output.append(f"\n📅 {date_formatted} ({day_label})")

            time_str = f"{time[:2]}:{time[2:]}"
            output.append(f"\n⏰ {time_str}")

            # Temperature (use TMP for short-term)
            if "TMP" in fc:
                output.append(f"  🌡️  {fc['TMP']}°C")

            # Sky condition
            if "SKY" in fc:
                sky_text = get_sky_condition(fc["SKY"])
                output.append(f"  ☁️  {sky_text}")

            # Precipitation type
            if "PTY" in fc and fc["PTY"] != "0":
                pty_text = get_precipitation_type(fc["PTY"])
                output.append(f"  ☔ {pty_text}")

            # Precipitation probability
            if "POP" in fc:
                output.append(f"  🌧️  강수확률: {fc['POP']}%")

            # Precipitation amount (6 hours)
            if "PCP" in fc:
                pcp = fc["PCP"]
                if pcp not in ["강수없음", "0"]:
                    output.append(f"  💧 강수량: {pcp}")
    else:
        # Single day view (existing logic)
        for key in filtered_keys:
            fc = forecasts[key]
            time = fc["time"]

            time_str = f"{time[:2]}:{time[2:]}"
            output.append(f"\n⏰ {time_str}")

            # Temperature (use TMP for short-term)
            if "TMP" in fc:
                output.append(f"  🌡️  {fc['TMP']}°C")

            # Sky condition
            if "SKY" in fc:
                sky_text = get_sky_condition(fc["SKY"])
                output.append(f"  ☁️  {sky_text}")

            # Precipitation type
            if "PTY" in fc and fc["PTY"] != "0":
                pty_text = get_precipitation_type(fc["PTY"])
                output.append(f"  ☔ {pty_text}")

            # Precipitation probability
            if "POP" in fc:
                output.append(f"  🌧️  강수확률: {fc['POP']}%")

            # Precipitation amount (6 hours)
            if "PCP" in fc:
                pcp = fc["PCP"]
                if pcp not in ["강수없음", "0"]:
                    output.append(f"  💧 강수량: {pcp}")

    return "\n".join(output)


def main():
    """Main entry point for forecast script."""
    parser = argparse.ArgumentParser(
        description="Get weather forecast from Korea Meteorological Administration"
    )

    # Subcommand
    parser.add_argument(
        "type",
        choices=["current", "ultrashort", "shortterm", "brief", "all"],
        help="Forecast type: current (현재), ultrashort (6시간), shortterm (3일), brief (현재+6시간), all (전체)"
    )

    # Location
    parser.add_argument(
        "--lat",
        type=float,
        required=True,
        help="Latitude in decimal degrees (e.g., 37.5665)"
    )
    parser.add_argument(
        "--lon",
        type=float,
        required=True,
        help="Longitude in decimal degrees (e.g., 126.9780)"
    )

    # Output options
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON response"
    )
    parser.add_argument(
        "--rows",
        type=int,
        default=300,
        help="Rows per API page (default: 300, auto-paginates if needed)"
    )
    parser.add_argument(
        "--days",
        default="1",
        help="Days from today for shortterm forecast ('all'=all available days, 1=tomorrow, 2=day after, 3=3 days later)"
    )

    args = parser.parse_args()

    # Validate --days argument
    if args.days != "all":
        try:
            days_int = int(args.days)
            if days_int < 1:
                print_error("--days must be 'all' or a positive integer")
                sys.exit(1)
        except ValueError:
            print_error("--days must be 'all' or a positive integer")
            sys.exit(1)

    try:
        if args.type == "all":
            # Fetch all forecast types
            results = {}
            for ftype in ["current", "ultrashort", "shortterm"]:
                results[ftype] = fetch_forecast(ftype, args.lat, args.lon, args.rows)

            if args.json:
                print(json.dumps(results, ensure_ascii=False, indent=2))
            else:
                nx, ny = latlon_to_grid(args.lat, args.lon)
                outputs = []
                outputs.append(format_current(results["current"], args.lat, args.lon, nx, ny))
                outputs.append("")
                outputs.append(format_ultrashort(results["ultrashort"], args.lat, args.lon, nx, ny))
                outputs.append("")
                outputs.append(format_shortterm(results["shortterm"], args.days, args.lat, args.lon, nx, ny))
                print("\n".join(outputs))

        elif args.type == "brief":
            # Fetch current + ultrashort
            results = {}
            for ftype in ["current", "ultrashort"]:
                results[ftype] = fetch_forecast(ftype, args.lat, args.lon, args.rows)

            if args.json:
                print(json.dumps(results, ensure_ascii=False, indent=2))
            else:
                nx, ny = latlon_to_grid(args.lat, args.lon)
                outputs = []
                outputs.append(format_current(results["current"], args.lat, args.lon, nx, ny))
                outputs.append("")
                outputs.append(format_ultrashort(results["ultrashort"], args.lat, args.lon, nx, ny))
                print("\n".join(outputs))
        else:
            # Fetch single forecast type
            data = fetch_forecast(args.type, args.lat, args.lon, args.rows)

            if args.json:
                print(json.dumps(data, ensure_ascii=False, indent=2))
            else:
                nx, ny = latlon_to_grid(args.lat, args.lon)
                if args.type == "current":
                    output = format_current(data, args.lat, args.lon, nx, ny)
                elif args.type == "ultrashort":
                    output = format_ultrashort(data, args.lat, args.lon, nx, ny)
                elif args.type == "shortterm":
                    output = format_shortterm(data, args.days, args.lat, args.lon, nx, ny)
                else:
                    output = "Unknown forecast type"
                print(output)

    except Exception as e:
        print_error(str(e))
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/grid_converter.py
content: #!/usr/bin/env python3
"""
KMA Grid Coordinate Converter
Convert between latitude/longitude and KMA grid coordinates (nx, ny).

Based on official KMA conversion algorithm using Lambert Conformal Conic Projection.

References:
- KMA Official Algorithm: https://gist.github.com/fronteer-kr/14d7f779d52a21ac2f16
- Improved Implementation: https://blog.naver.com/seoulworkshop/222873041453
"""

import math
from typing import Tuple


# Lambert Conformal Conic Projection Constants
EARTH_RADIUS_KM = 6371.00877   # Earth radius (km)
GRID_SPACING_KM = 5.0          # Grid spacing (km)
PROJ_LAT_1 = 30.0              # Standard parallel 1 (degrees)
PROJ_LAT_2 = 60.0              # Standard parallel 2 (degrees)
REF_LON = 126.0                # Reference longitude (degrees)
REF_LAT = 38.0                 # Reference latitude (degrees)
REF_X = 43.0                   # Reference point X coordinate
REF_Y = 136.0                  # Reference point Y coordinate

DEGREE_TO_RADIAN = math.pi / 180.0
GRID_UNIT_COUNT = EARTH_RADIUS_KM / GRID_SPACING_KM


def latlon_to_grid(lat: float, lon: float) -> Tuple[int, int]:
    """
    Convert latitude/longitude to KMA grid coordinates.

    Args:
        lat: Latitude (degrees)
        lon: Longitude (degrees)

    Returns:
        Tuple[int, int]: (nx, ny) grid coordinates

    Example:
        >>> latlon_to_grid(37.5665, 126.9780)  # Seoul City Hall
        (60, 127)
    """
    # Convert to radians
    proj_lat_1_rad = PROJ_LAT_1 * DEGREE_TO_RADIAN
    proj_lat_2_rad = PROJ_LAT_2 * DEGREE_TO_RADIAN
    ref_lon_rad = REF_LON * DEGREE_TO_RADIAN
    ref_lat_rad = REF_LAT * DEGREE_TO_RADIAN
    lat_rad = lat * DEGREE_TO_RADIAN
    lon_rad = lon * DEGREE_TO_RADIAN

    # Calculate projection parameters
    sn = math.log(math.cos(proj_lat_1_rad) / math.cos(proj_lat_2_rad)) / \
         math.log(math.tan(math.pi * 0.25 + proj_lat_2_rad * 0.5) /
                  math.tan(math.pi * 0.25 + proj_lat_1_rad * 0.5))

    sf = math.pow(math.tan(math.pi * 0.25 + proj_lat_1_rad * 0.5), sn) * \
         math.cos(proj_lat_1_rad) / sn

    ro = GRID_UNIT_COUNT * sf / math.pow(math.tan(math.pi * 0.25 + ref_lat_rad * 0.5), sn)

    # Calculate distance and angle
    distance = GRID_UNIT_COUNT * sf / math.pow(math.tan(math.pi * 0.25 + lat_rad * 0.5), sn)
    theta_rad = lon_rad - ref_lon_rad

    # Normalize theta to [-π, π]
    if theta_rad > math.pi:
        theta_rad -= 2.0 * math.pi
    if theta_rad < -math.pi:
        theta_rad += 2.0 * math.pi

    theta_rad *= sn

    # Calculate grid coordinates
    nx = int(distance * math.sin(theta_rad) + REF_X + 0.5)
    ny = int(ro - distance * math.cos(theta_rad) + REF_Y + 0.5)

    return nx, ny


def grid_to_latlon(nx: int, ny: int) -> Tuple[float, float]:
    """
    Convert KMA grid coordinates to latitude/longitude (inverse conversion).

    Args:
        nx: Grid X coordinate
        ny: Grid Y coordinate

    Returns:
        Tuple[float, float]: (latitude, longitude) in degrees

    Example:
        >>> grid_to_latlon(60, 127)  # Seoul City Hall
        (37.5665, 126.9780)
    """
    # Convert to radians
    proj_lat_1_rad = PROJ_LAT_1 * DEGREE_TO_RADIAN
    proj_lat_2_rad = PROJ_LAT_2 * DEGREE_TO_RADIAN
    ref_lon_rad = REF_LON * DEGREE_TO_RADIAN
    ref_lat_rad = REF_LAT * DEGREE_TO_RADIAN

    # Calculate projection parameters
    sn = math.log(math.cos(proj_lat_1_rad) / math.cos(proj_lat_2_rad)) / \
         math.log(math.tan(math.pi * 0.25 + proj_lat_2_rad * 0.5) /
                  math.tan(math.pi * 0.25 + proj_lat_1_rad * 0.5))

    sf = math.pow(math.tan(math.pi * 0.25 + proj_lat_1_rad * 0.5), sn) * \
         math.cos(proj_lat_1_rad) / sn

    ro = GRID_UNIT_COUNT * sf / math.pow(math.tan(math.pi * 0.25 + ref_lat_rad * 0.5), sn)

    # Calculate differences from reference point
    diff_x = nx - REF_X
    diff_y = ro - ny + REF_Y

    # Calculate distance
    distance = math.sqrt(diff_x * diff_x + diff_y * diff_y)
    if sn < 0.0:
        distance = -distance

    # Calculate latitude
    lat_rad = 2.0 * math.atan(math.pow(GRID_UNIT_COUNT * sf / distance, 1.0 / sn)) - math.pi * 0.5

    # Calculate longitude
    if abs(diff_x) <= 0.0:
        theta_rad = 0.0
    else:
        if abs(diff_y) <= 0.0:
            theta_rad = math.pi * 0.5 if diff_x >= 0.0 else -math.pi * 0.5
        else:
            theta_rad = math.atan2(diff_x, diff_y)

    lon_rad = theta_rad / sn + ref_lon_rad

    # Convert to degrees
    lat = lat_rad / DEGREE_TO_RADIAN
    lon = lon_rad / DEGREE_TO_RADIAN

    return lat, lon


if __name__ == "__main__":
    # Simple test with Seoul coordinates
    import sys

    if len(sys.argv) == 3:
        # Command-line usage: python grid_converter.py <lat> <lon>
        try:
            lat = float(sys.argv[1])
            lon = float(sys.argv[2])
            nx, ny = latlon_to_grid(lat, lon)
            print(f"Lat/Lon ({lat}, {lon}) -> Grid: ({nx}, {ny})")

            # Test inverse conversion
            lat_back, lon_back = grid_to_latlon(nx, ny)
            print(f"Grid ({nx}, {ny}) -> Lat/Lon: ({lat_back:.4f}, {lon_back:.4f})")
        except ValueError:
            print("Usage: python grid_converter.py <latitude> <longitude>")
            sys.exit(1)
    else:
        # Basic test
        print("Testing grid_converter.py")
        print("-" * 40)

        # Seoul City Hall
        lat, lon = 37.5665, 126.9780
        nx, ny = latlon_to_grid(lat, lon)
        print(f"Forward:  Lat/Lon ({lat}, {lon}) -> Grid ({nx}, {ny})")

        # Test inverse conversion
        lat_back, lon_back = grid_to_latlon(nx, ny)
        print(f"Inverse:  Grid ({nx}, {ny}) -> Lat/Lon ({lat_back:.4f}, {lon_back:.4f})")
        print(f"Error:    ({abs(lat - lat_back):.6f}, {abs(lon - lon_back):.6f})")

        print("\nUsage: python grid_converter.py <latitude> <longitude>")


path: scripts/kma_api.py
content: #!/usr/bin/env python3
"""
KMA (Korea Meteorological Administration) API Common Utilities

Provides shared functionality for all KMA API scripts:
- API key management via environment variables
- HTTP request handling with error handling
- Response validation

Used by forecast.py, warnings.py, and midterm.py
"""

import os
import sys
import json
import urllib.request
import urllib.parse
import urllib.error
from typing import Dict, Optional


def get_service_key() -> str:
    """
    Load and validate KMA API service key from environment variable.

    Returns:
        str: The API service key

    Raises:
        ValueError: If KMA_SERVICE_KEY environment variable is not set

    Example:
        >>> service_key = get_service_key()
    """
    service_key = os.environ.get("KMA_SERVICE_KEY")
    if not service_key:
        raise ValueError(
            "KMA API service key not found. "
            "Set KMA_SERVICE_KEY environment variable.\n"
            "Get your key at: https://www.data.go.kr"
        )
    return service_key


def fetch_api(
    url: str,
    params: Dict[str, str],
    service_key: Optional[str] = None
) -> Dict:
    """
    Generic API request handler for KMA APIs.

    Automatically adds serviceKey to parameters, handles HTTP requests,
    and validates API responses.

    Args:
        url: Full API endpoint URL
        params: Query parameters (serviceKey will be added automatically)
        service_key: Optional API key (defaults to environment variable)

    Returns:
        dict: Parsed JSON response from API

    Raises:
        Exception: On API errors or HTTP errors

    Example:
        >>> data = fetch_api(
        ...     "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst",
        ...     {"base_date": "20260201", "base_time": "0600", "nx": "60", "ny": "127"}
        ... )
    """
    # Add service key to parameters
    params["serviceKey"] = service_key or get_service_key()

    # Build full URL with query parameters
    full_url = f"{url}?{urllib.parse.urlencode(params)}"

    # Make HTTP request
    try:
        with urllib.request.urlopen(full_url) as response:
            if response.status != 200:
                raise Exception(f"HTTP Error: {response.status}")

            # Parse JSON response
            data = json.loads(response.read().decode("utf-8"))

            # Validate API response
            if "response" in data:
                header = data["response"].get("header", {})
                result_code = header.get("resultCode")

                if result_code != "00":
                    result_msg = header.get("resultMsg", "Unknown error")
                    raise Exception(
                        f"API Error {result_code}: {result_msg}"
                    )

            return data

    except urllib.error.HTTPError as e:
        error_body = e.read().decode("utf-8", errors="ignore")
        raise Exception(
            f"API request failed: HTTP {e.code}\n"
            f"Response: {error_body[:200]}"
        )
    except urllib.error.URLError as e:
        raise Exception(f"Network error: {e.reason}")


def print_error(message: str) -> None:
    """
    Print error message to stderr in a user-friendly format.

    Args:
        message: Error message to display
    """
    import sys
    print(f"Error: {message}", file=sys.stderr)


if __name__ == "__main__":
    # Test the module
    print("KMA API Utilities Module")
    print("=" * 40)

    # Test service key loading
    try:
        key = get_service_key()
        print(f"✓ Service key loaded successfully")
    except ValueError as e:
        print(f"✗ {e}")


path: scripts/midterm.py
content: #!/usr/bin/env python3
"""
KMA Mid-term Forecast

Retrieves mid-term weather forecasts (3-10 days) from Korea Meteorological Administration.

Released at 06:00 and 18:00 (KST).

Requires KMA_SERVICE_KEY environment variable.
"""

import sys
import json
import argparse
from datetime import datetime, timedelta
from typing import Dict

# Import local modules
try:
    from kma_api import fetch_api, print_error
except ImportError:
    # Try relative import if run as script
    import os
    sys.path.insert(0, os.path.dirname(__file__))
    from kma_api import fetch_api, print_error


# API Configuration
BASE_URL = "https://apis.data.go.kr/1360000/MidFcstInfoService"
ENDPOINT = "/getMidFcst"


# Station ID mapping for getMidFcst API (from official KMA documentation)
# Uses stnId parameter (not regId)
# Returns plain text forecast (wfSv field) for LLM interpretation
STATION_CODES = {
    "전국": "108",      # 전국 (National overview)
    "서울": "109",      # 서울, 인천, 경기도
    "경기": "109",      # 서울, 인천, 경기도
    "인천": "109",      # 서울, 인천, 경기도
    "강원": "105",      # 강원도
    "충북": "131",      # 충청북도
    "충남": "133",      # 대전, 세종, 충청남도
    "대전": "133",      # 대전, 세종, 충청남도
    "세종": "133",      # 대전, 세종, 충청남도
    "전북": "146",      # 전북자치도
    "광주": "156",      # 광주, 전라남도
    "전남": "156",      # 광주, 전라남도
    "대구": "143",      # 대구, 경상북도
    "경북": "143",      # 대구, 경상북도
    "부산": "159",      # 부산, 울산, 경상남도
    "울산": "159",      # 부산, 울산, 경상남도
    "경남": "159",      # 부산, 울산, 경상남도
    "제주": "184",      # 제주도
}


def get_midterm_time() -> str:
    """
    Calculate tmFc (release time) for mid-term forecast API.

    Mid-term forecasts are released at 06:00 and 18:00 KST.

    Returns:
        str: tmFc in format YYYYMMDDHHmm

    Example:
        >>> get_midterm_time()
        '202602010600'
    """
    now = datetime.now()

    # Determine which release time to use
    if now.hour < 6:
        # Use yesterday's 18:00
        release_time = now - timedelta(days=1)
        release_hour = 18
    elif now.hour < 18:
        # Use today's 06:00
        release_time = now
        release_hour = 6
    else:
        # Use today's 18:00
        release_time = now
        release_hour = 18

    tm_fc = release_time.strftime(f"%Y%m%d{release_hour:02d}00")
    return tm_fc


def fetch_midterm_forecast(stn_id: str, tm_fc: str = None) -> Dict:
    """
    Fetch mid-term forecast from KMA API.

    Args:
        stn_id: Station ID (e.g., "109" for Seoul·Gyeonggi)
        tm_fc: Release time in YYYYMMDDHHmm format (optional, auto-calculated)

    Returns:
        dict: API response data

    Raises:
        Exception: On API errors
    """
    if tm_fc is None:
        tm_fc = get_midterm_time()

    url = f"{BASE_URL}{ENDPOINT}"

    params = {
        "numOfRows": "10",
        "pageNo": "1",
        "dataType": "JSON",
        "stnId": stn_id,  # Changed from regId to stnId
        "tmFc": tm_fc,
    }

    return fetch_api(url, params)


def format_midterm(data: Dict) -> str:
    """
    Format mid-term forecast data from plain text response.

    Args:
        data: API response data

    Returns:
        str: Formatted text output (plain text from wfSv field)
    """
    try:
        items = data["response"]["body"]["items"]["item"]
    except (KeyError, TypeError):
        return "Error: Invalid API response format"

    if not items:
        return "No data available"

    # Mid-term forecast returns a single item with wfSv (plain text) field
    if isinstance(items, dict):
        item = items
    elif isinstance(items, list):
        item = items[0]
    else:
        return "Error: Unexpected data format"

    # Build output
    output = []
    output.append("📅 중기예보")

    # Release time
    if "tmFc" in item:
        tm_fc = item["tmFc"]
        tm_formatted = f"{tm_fc[:4]}-{tm_fc[4:6]}-{tm_fc[6:8]} {tm_fc[8:10]}:{tm_fc[10:12]}"
        output.append(f"발표시각: {tm_formatted}")
    output.append("")

    # Plain text forecast from wfSv field
    if "wfSv" in item:
        output.append(item["wfSv"])
    else:
        output.append("Error: No forecast text (wfSv) in response")

    return "\n".join(output)


def main():
    """Main entry point for midterm forecast script."""
    parser = argparse.ArgumentParser(
        description="Get mid-term forecast (3-10 days) from Korea Meteorological Administration"
    )

    # Region selection
    region_group = parser.add_mutually_exclusive_group(required=False)
    region_group.add_argument(
        "--region",
        choices=list(STATION_CODES.keys()),
        help="Region name (Korean)"
    )
    region_group.add_argument(
        "--stn-id",
        help="Station ID (e.g., 109 for Seoul·Gyeonggi)"
    )

    # Time
    parser.add_argument(
        "--tm-fc",
        help="Release time in YYYYMMDDHHmm format (optional, auto-calculated)"
    )

    # Output options
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON response"
    )

    # List regions
    parser.add_argument(
        "--list-regions",
        action="store_true",
        help="List all available regions and exit"
    )

    args = parser.parse_args()

    # List regions
    if args.list_regions:
        print("Available Regions:")
        print("=" * 50)
        for name, code in sorted(STATION_CODES.items()):
            print(f"  {name:10s} → {code}")
        sys.exit(0)

    # Determine station ID
    if not args.region and not args.stn_id:
        parser.error("one of the arguments --region --stn-id is required")

    if args.region:
        stn_id = STATION_CODES[args.region]
    else:
        stn_id = args.stn_id

    try:
        # Fetch mid-term forecast
        data = fetch_midterm_forecast(stn_id, args.tm_fc)

        if args.json:
            # Output raw JSON
            print(json.dumps(data, ensure_ascii=False, indent=2))
        else:
            # Format and output text
            output = format_midterm(data)
            print(output)

    except Exception as e:
        print_error(str(e))
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/weather_warnings.py
content: #!/usr/bin/env python3
"""
KMA Weather Warnings Status

Retrieves current weather warnings status from Korea Meteorological Administration.

Shows nationwide summary of active warnings and advisories.

Requires KMA_SERVICE_KEY environment variable.
"""

import sys
import json
import argparse
from typing import Dict

# Import local modules
try:
    from kma_api import fetch_api, print_error
except ImportError:
    # Try relative import if run as script
    import os
    sys.path.insert(0, os.path.dirname(__file__))
    from kma_api import fetch_api, print_error


# API Configuration
BASE_URL = "https://apis.data.go.kr/1360000/WthrWrnInfoService"
ENDPOINT = "/getPwnStatus"


def fetch_warning_status() -> Dict:
    """
    Fetch current weather warning status from KMA API.

    Returns:
        dict: API response data

    Raises:
        Exception: On API errors
    """
    url = f"{BASE_URL}{ENDPOINT}"

    params = {
        "numOfRows": "10",
        "pageNo": "1",
        "dataType": "JSON",
    }

    return fetch_api(url, params)


def format_warning_status(data: Dict) -> str:
    """
    Format weather warning status data.

    Args:
        data: API response data

    Returns:
        str: Formatted text output
    """
    try:
        items = data["response"]["body"]["items"]["item"]
    except (KeyError, TypeError):
        return "Error: Invalid API response format"

    # Handle single item (dict) or multiple items (list)
    if isinstance(items, dict):
        item = items
    elif isinstance(items, list):
        item = items[0]
    else:
        return "✅ 현재 발효 중인 기상특보가 없습니다."

    # Build output
    output = []
    output.append("🚨 기상특보 현황")

    # Issue time
    if "tmFc" in item:
        tm_fc = str(item["tmFc"]).zfill(12)
        tm_formatted = f"{tm_fc[:4]}-{tm_fc[4:6]}-{tm_fc[6:8]} {tm_fc[8:10]}:{tm_fc[10:12]}"
        output.append(f"발표시각: {tm_formatted}")

    # Effective time
    if "tmEf" in item:
        tm_ef = str(item["tmEf"]).zfill(12)
        tm_ef_formatted = f"{tm_ef[:4]}-{tm_ef[4:6]}-{tm_ef[6:8]} {tm_ef[8:10]}:{tm_ef[10:12]}"
        output.append(f"발효시각: {tm_ef_formatted}")

    output.append("")

    # Current warnings (t6)
    if "t6" in item and item["t6"]:
        output.append("📍 현재 발효 중인 특보")
        t6_content = item["t6"].strip()
        if t6_content and t6_content != "o 없음":
            # Parse and format t6 content
            lines = t6_content.split("o ")
            for line in lines:
                line = line.strip()
                if line and line != "없음":
                    output.append(f"  • {line}")
        else:
            output.append("  ✅ 없음")
        output.append("")

    # Preliminary warnings (t7)
    if "t7" in item and item["t7"]:
        output.append("⚠️  예비특보")
        t7_content = item["t7"].strip()
        if t7_content and t7_content != "o 없음":
            # Parse and format t7 content
            lines = t7_content.split("o ")
            for line in lines:
                line = line.strip()
                if line and line != "없음":
                    output.append(f"  • {line}")
        else:
            output.append("  ✅ 없음")
        output.append("")

    # Other info
    if "other" in item and item["other"]:
        other_content = item["other"].strip()
        if other_content and other_content != "o 없음":
            output.append("ℹ️  기타")
            lines = other_content.split("o ")
            for line in lines:
                line = line.strip()
                if line and line != "없음":
                    output.append(f"  • {line}")

    return "\n".join(output)


def main():
    """Main entry point for warnings status script."""
    parser = argparse.ArgumentParser(
        description="Get current weather warnings status from Korea Meteorological Administration"
    )

    # Output options
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON response"
    )

    args = parser.parse_args()

    try:
        # Fetch warning status
        data = fetch_warning_status()

        if args.json:
            # Output raw JSON
            print(json.dumps(data, ensure_ascii=False, indent=2))
        else:
            # Format and output text
            output = format_warning_status(data)
            print(output)

    except Exception as e:
        print_error(str(e))
        sys.exit(1)


if __name__ == "__main__":
    main()


path: references/api-forecast.md
content: # Short-term Forecast API Reference

KMA short-term forecast service provides three types of forecasts using the same grid coordinate system.

**Base URL**: `https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0`

**Service**: 기상청 단기예보 조회서비스 (15084084)

## Endpoints

### 1. Ultra-short-term Observation (초단기실황)

**Endpoint**: `/getUltraSrtNcst`

**Description**: Current weather observations (실시간 관측 데이터)

**Release Schedule**: Every hour at :40 minutes

**Base Time Format**: HH00 (on the hour, e.g., 0600, 1400)

**Data Availability**: ~10 minutes after release (:40-:50 each hour)

**Parameters**:

| Parameter | Required | Type | Description | Example |
|-----------|----------|------|-------------|---------|
| serviceKey | Yes | String | API service key | (your key) |
| numOfRows | Yes | Integer | Number of rows per page | 10 |
| pageNo | Yes | Integer | Page number | 1 |
| dataType | Yes | String | Response format (XML/JSON) | JSON |
| base_date | Yes | String | Base date (YYYYMMDD) | 20260201 |
| base_time | Yes | String | Base time (HHmm) | 0600 |
| nx | Yes | Integer | Grid X coordinate | 60 |
| ny | Yes | Integer | Grid Y coordinate | 127 |

**Response Categories** (초단기실황 카테고리):

| Category | Name | Unit | Description |
|----------|------|------|-------------|
| T1H | Temperature | °C | Current temperature |
| RN1 | 1-hour precipitation | mm | Precipitation in the last hour |
| UUU | East-west wind | m/s | Wind component (east-west) |
| VVV | North-south wind | m/s | Wind component (north-south) |
| REH | Humidity | % | Relative humidity |
| PTY | Precipitation type | Code | 0=None, 1=Rain, 2=Rain/Snow, 3=Snow, 4=Shower |
| VEC | Wind direction | deg | Wind direction in degrees (0-360) |
| WSD | Wind speed | m/s | Wind speed |

**Example Request**:
```bash
curl "https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst?serviceKey=YOUR_KEY&numOfRows=10&pageNo=1&dataType=JSON&base_date=20260201&base_time=0600&nx=60&ny=127"
```

---

### 2. Ultra-short-term Forecast (초단기예보)

**Endpoint**: `/getUltraSrtFcst`

**Description**: Forecast for the next 6 hours (6시간 예보)

**Release Schedule**: Every hour at :45 minutes

**Base Time Format**: HH30 (30 min past the hour, e.g., 0630, 1430)

**Data Availability**: ~10 minutes after release (:45-:55 each hour)

**Parameters**: Same as ultra-short-term observation

**Response Categories** (초단기예보 카테고리):

| Category | Name | Unit | Description |
|----------|------|------|-------------|
| T1H | Temperature | °C | Hourly temperature |
| RN1 | 1-hour precipitation | mm | Expected precipitation per hour |
| SKY | Sky condition | Code | 1=Clear, 3=Partly cloudy, 4=Cloudy |
| UUU | East-west wind | m/s | Wind component (east-west) |
| VVV | North-south wind | m/s | Wind component (north-south) |
| REH | Humidity | % | Relative humidity |
| PTY | Precipitation type | Code | 0=None, 1=Rain, 2=Rain/Snow, 3=Snow, 4=Shower |
| LGT | Lightning | Code | 0=None, 1-3=Lightning probability |
| VEC | Wind direction | deg | Wind direction in degrees (0-360) |
| WSD | Wind speed | m/s | Wind speed |

**Example Request**:
```bash
curl "https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst?serviceKey=YOUR_KEY&numOfRows=60&pageNo=1&dataType=JSON&base_date=20260201&base_time=0600&nx=60&ny=127"
```

---

### 3. Short-term Forecast (단기예보)

**Endpoint**: `/getVilageFcst`

**Description**: Forecast for the next 3 days (3일 예보)

**Release Schedule**: 8 times daily at 02:10, 05:10, 08:10, 11:10, 14:10, 17:10, 20:10, 23:10 (KST)

**Parameters**: Same as ultra-short-term observation

**Response Categories** (단기예보 카테고리):

| Category | Name | Unit | Description |
|----------|------|------|-------------|
| POP | Precipitation probability | % | Probability of precipitation |
| PTY | Precipitation type | Code | 0=None, 1=Rain, 2=Rain/Snow, 3=Snow, 4=Shower |
| PCP | Precipitation amount | mm | 1-hour precipitation (강수없음 = no rain) |
| REH | Humidity | % | Relative humidity |
| SNO | Snowfall | cm | 1-hour snowfall (적설없음 = no snow) |
| SKY | Sky condition | Code | 1=Clear, 3=Partly cloudy, 4=Cloudy |
| TMP | Temperature | °C | Hourly temperature |
| TMN | Min temperature | °C | Daily minimum temperature |
| TMX | Max temperature | °C | Daily maximum temperature |
| UUU | East-west wind | m/s | Wind component (east-west) |
| VVV | North-south wind | m/s | Wind component (north-south) |
| WAV | Wave height | m | Wave height |
| VEC | Wind direction | deg | Wind direction in degrees (0-360) |
| WSD | Wind speed | m/s | Wind speed |

**Example Request**:
```bash
curl "https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst?serviceKey=YOUR_KEY&numOfRows=100&pageNo=1&dataType=JSON&base_date=20260201&base_time=0500&nx=60&ny=127"
```

---

## Grid Coordinate System

KMA uses a **Lambert Conformal Conic projection** with 5km×5km grid cells.

**Projection Parameters**:
- Earth radius: 6371.00877 km
- Grid spacing: 5.0 km
- Standard parallel 1: 30°N
- Standard parallel 2: 60°N
- Reference longitude: 126°E
- Reference latitude: 38°N
- Reference point: (43, 136)

**Conversion**: Use `grid_converter.py` to convert latitude/longitude to grid coordinates (nx, ny).

Example:
```bash
python3 grid_converter.py 37.5665 126.9780
# Output: Grid: (60, 127)
```

---

## Response Format

All endpoints return JSON (or XML) in this structure:

```json
{
  "response": {
    "header": {
      "resultCode": "00",
      "resultMsg": "NORMAL_SERVICE"
    },
    "body": {
      "dataType": "JSON",
      "items": {
        "item": [
          {
            "baseDate": "20260201",
            "baseTime": "0600",
            "category": "T1H",
            "nx": 60,
            "ny": 127,
            "obsrValue": "5.2"  // or "fcstValue" for forecasts
          },
          ...
        ]
      },
      "pageNo": 1,
      "numOfRows": 10,
      "totalCount": 8
    }
  }
}
```

**Key Fields**:
- `baseDate`, `baseTime`: When the forecast was issued
- `fcstDate`, `fcstTime`: When the forecast applies (forecast endpoints only)
- `category`: Weather element code (T1H, SKY, etc.)
- `obsrValue`: Observed value (observation endpoint)
- `fcstValue`: Forecast value (forecast endpoints)

---

## Error Codes

| Code | Message | Description | Solution |
|------|---------|-------------|----------|
| 00 | NORMAL_SERVICE | Success | - |
| 01 | APPLICATION_ERROR | Application error | Check request parameters |
| 03 | DB_ERROR | Database error | Retry later |
| 04 | NODATA_ERROR | No data available | Check date/time, grid coordinates |
| 05 | SERVICETIMEOUT_ERROR | Service timeout | Retry later |
| 10 | INVALID_REQUEST_PARAMETER_ERROR | Invalid parameter | Check parameter format |
| 11 | NO_MANDATORY_REQUEST_PARAMETERS_ERROR | Missing parameter | Add required parameters |
| 12 | NO_OPENAPI_SERVICE_ERROR | Service not found | Check endpoint URL |
| 20 | SERVICE_ACCESS_DENIED_ERROR | Access denied | Check service key |
| 22 | LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR | Rate limit exceeded | Wait and retry |
| 30 | SERVICE_KEY_IS_NOT_REGISTERED_ERROR | Invalid service key | Check your API key |
| 31 | DEADLINE_HAS_EXPIRED_ERROR | Service key expired | Renew your API key |
| 32 | UNREGISTERED_IP_ERROR | IP not registered | Register IP in portal |
| 33 | UNSIGNED_CALL_ERROR | Unsigned request | Check request format |
| 99 | UNKNOWN_ERROR | Unknown error | Contact support |

---

## Usage Notes

1. **Base Time Calculation**:
   - For current: Use HH00 format. If before :40, use previous hour
   - For ultra-short: Use HH30 format. If before :45, use previous hour's :30
   - For short-term: Use the most recent release time (02, 05, 08, 11, 14, 17, 20, 23)

2. **Data Availability**:
   - Data becomes available ~10 minutes after base_time
   - Historical data may not be available for all base times

3. **Grid Coverage**:
   - Grid coordinates cover South Korea only
   - Invalid coordinates will return NODATA_ERROR

4. **Pagination**:
   - Increase `numOfRows` to get more data points
   - Default: 10, Max: varies by service (typically 1000)

5. **Category Filtering**:
   - API returns all categories for the given time/location
   - Filter by `category` field in application code

---

## References

- Official API page: https://www.data.go.kr/data/15084084/openapi.do
- API documentation (Korean): Available after service subscription
- Grid conversion reference: https://gist.github.com/fronteer-kr/14d7f779d52a21ac2f16


path: references/api-midterm.md
content: # Mid-term Forecast API Reference

KMA mid-term forecast service provides weather outlook for 3-10 days ahead.

**Base URL**: `https://apis.data.go.kr/1360000/MidFcstInfoService`

**Service**: 기상청 중기예보 조회서비스 (15059468)

## Endpoints

### 1. Get Mid-term Forecast

**Endpoint**: `/getMidFcst`

**Description**: Retrieve mid-term forecast (3-10 days) for a specific region

**Release Schedule**: Twice daily at **06:00** and **18:00** (KST)

**Parameters**:

| Parameter | Required | Type | Description | Example |
|-----------|----------|------|-------------|---------|
| serviceKey | Yes | String | API service key | (your key) |
| numOfRows | Yes | Integer | Number of rows per page | 10 |
| pageNo | Yes | Integer | Page number | 1 |
| dataType | Yes | String | Response format (XML/JSON) | JSON |
| stnId | Yes | String | Station ID (3-digit code) | 109 |
| tmFc | Yes | String | Release time (YYYYMMDDHHmm) | 202602010600 |

**Example Request**:
```bash
curl "https://apis.data.go.kr/1360000/MidFcstInfoService/getMidFcst?serviceKey=YOUR_KEY&numOfRows=10&pageNo=1&dataType=JSON&stnId=109&tmFc=202602010600"
```

**Note**: This endpoint returns **plain text forecasts** in the `wfSv` field, designed for natural language interpretation.

---

## Station Codes

The mid-term forecast API uses simplified 3-digit station codes:

| Code | Region Coverage | Korean |
|------|-----------------|--------|
| 108 | National | 전국 |
| 109 | Seoul, Incheon, Gyeonggi | 서울, 인천, 경기도 |
| 105 | Gangwon | 강원도 |
| 131 | Chungcheongbuk | 충청북도 |
| 133 | Daejeon, Sejong, Chungcheongnam | 대전, 세종, 충청남도 |
| 146 | Jeonbuk | 전북자치도 |
| 156 | Gwangju, Jeonnam | 광주, 전라남도 |
| 143 | Daegu, Gyeongbuk | 대구, 경상북도 |
| 159 | Busan, Ulsan, Gyeongnam | 부산, 울산, 경상남도 |
| 184 | Jeju | 제주도 |

**Note**: Each station code covers a broad region. For city-level detail, use the short-term forecast API with grid coordinates.

---

## Response Format

**JSON Structure**:
```json
{
  "response": {
    "header": {
      "resultCode": "00",
      "resultMsg": "NORMAL_SERVICE"
    },
    "body": {
      "dataType": "JSON",
      "items": {
        "item": {
          "stnId": "109",
          "tmFc": "202602010600",
          "wfSv": "○ (날씨) 28일(수)~3월 2일(일)은 고기압의 영향으로 대체로 맑겠으나, 3일(월)~5일(수)은 기압골의 영향으로 구름많고 3일(월)에는 눈 또는 비가 오겠습니다.\n\n○ (아침기온) 평년(-6~2도)과 비슷하거나 조금 낮겠습니다.\n○ (낮기온) 평년(4~11도)과 비슷하거나 조금 높겠습니다."
        }
      },
      "pageNo": 1,
      "numOfRows": 10,
      "totalCount": 1
    }
  }
}
```

**Key Fields**:
- `stnId`: Station code (e.g., "109" for Seoul·Gyeonggi)
- `tmFc`: Release time in YYYYMMDDHHmm format
- `wfSv`: **Plain text forecast** in Korean, containing:
  - General weather outlook for the 3-10 day period
  - Expected temperatures compared to normal ranges
  - Precipitation expectations
  - Notable weather patterns (high/low pressure systems, etc.)

---

## Response Field Details

### Plain Text Forecast (wfSv)

The `wfSv` field contains a **natural language forecast** written by KMA meteorologists. This text typically includes:

**Structure**:
1. **Weather Overview** (날씨)
   - General weather patterns for the forecast period
   - Expected conditions day by day
   - Precipitation events (rain/snow)
   - Weather system influences (high pressure, low pressure, fronts)

2. **Morning Temperature** (아침기온)
   - Expected morning lows
   - Comparison to normal temperatures (평년)
   - Normal range provided in parentheses

3. **Afternoon Temperature** (낮기온)
   - Expected afternoon highs
   - Comparison to normal temperatures (평년)
   - Normal range provided in parentheses

**Example Content**:
```
○ (날씨) 28일(수)~3월 2일(일)은 고기압의 영향으로 대체로 맑겠으나,
3일(월)~5일(수)은 기압골의 영향으로 구름많고 3일(월)에는 눈 또는 비가 오겠습니다.

○ (아침기온) 평년(-6~2도)과 비슷하거나 조금 낮겠습니다.
○ (낮기온) 평년(4~11도)과 비슷하거나 조금 높겠습니다.
```

**Interpreting the Text**:
- Use LLMs to parse and extract key information
- Look for keywords: 맑음 (clear), 구름많음 (cloudy), 비 (rain), 눈 (snow)
- Temperature comparisons: 비슷하다 (similar), 높다 (higher), 낮다 (lower)
- Time indicators: Day of week and dates

---

## Usage Notes

### Release Time Calculation

Mid-term forecasts are issued at **06:00** and **18:00** KST:

```python
from datetime import datetime

now = datetime.now()

if now.hour < 6:
    # Use yesterday's 18:00
    tm_fc = (now - timedelta(days=1)).strftime("%Y%m%d1800")
elif now.hour < 18:
    # Use today's 06:00
    tm_fc = now.strftime("%Y%m%d0600")
else:
    # Use today's 18:00
    tm_fc = now.strftime("%Y%m%d1800")
```

### Forecast Coverage

- **Period**: 3-10 days ahead (general outlook)
- **Format**: Plain text narrative, not structured data
- **Detail Level**: Less specific than short-term forecasts
- **Best Use**: General planning and trend awareness

### Data Availability

- Forecasts are typically available **~30 minutes** after release time
- Historical forecasts may not be available for all regions
- Use most recent `tmFc` for current forecast

### Station Selection

- Use station code (e.g., `109` for Seoul·Gyeonggi region)
- Each code covers a broad geographic area
- For city-level or precise location forecasts, use short-term forecast API with grid coordinates

---

## Integration Example

**Get mid-term forecast for Seoul**:
```python
from midterm import fetch_midterm_forecast, format_midterm

# Fetch forecast (auto-calculates tmFc)
data = fetch_midterm_forecast("109")  # Seoul·Gyeonggi

# Display formatted output
print(format_midterm(data))
```

**Extract plain text forecast**:
```python
item = data["response"]["body"]["items"]["item"]

# Get the plain text forecast
forecast_text = item["wfSv"]
print(forecast_text)

# Parse with LLM or text processing
# Example output:
# ○ (날씨) 28일(수)~3월 2일(일)은 고기압의 영향으로 대체로 맑겠으나...
# ○ (아침기온) 평년(-6~2도)과 비슷하거나 조금 낮겠습니다.
# ○ (낮기온) 평년(4~11도)과 비슷하거나 조금 높겠습니다.
```

**Process with LLM for structured extraction**:
```python
# Use an LLM to extract structured information from the text
prompt = f"""
Extract weather information from this Korean forecast:
{forecast_text}

Provide:
1. Overall weather pattern
2. Precipitation days
3. Temperature trends
"""
# Process with your LLM of choice
```

---

## Error Codes

See [api-forecast.md](api-forecast.md#error-codes) for common error codes.

**Mid-term specific errors**:
- **NODATA_ERROR (04)**: Invalid `stnId` or `tmFc` not yet released
- **INVALID_REQUEST_PARAMETER_ERROR (10)**: Check `stnId` format (must be 3-digit code) and `tmFc` time

---

## Best Practices

1. **Auto-calculate tmFc**: Use helper function to determine most recent release time
2. **Cache forecasts**: Mid-term forecasts change only twice daily - cache for up to 6 hours
3. **Station mapping**: Provide user-friendly region names, map to station codes internally
4. **Combine with short-term**: Use short-term for Days 1-3, mid-term for Days 4-10
5. **LLM Processing**: Use LLMs to parse the plain text forecast for structured information extraction
6. **Present原文**: Show the original Korean text to users for full detail

---

## Comparison with Short-term Forecast

| Feature | Short-term (단기예보) | Mid-term (중기예보) |
|---------|---------------------|-------------------|
| Coverage | 3 days (72 hours) | 3-10 days |
| Resolution | Hourly | Daily overview |
| Updates | 8 times daily | 2 times daily |
| Format | Structured data (codes) | Plain text narrative |
| Location | 5km×5km grid coordinates | Regional station codes |
| Best for | Detailed planning (1-3 days) | General outlook (4-10 days) |

**Recommendation**: Use both APIs complementarily:
- Short-term for detailed hourly forecasts (Days 1-3)
- Mid-term for general outlook (Days 4-10)

---

## References

- Official API page: https://www.data.go.kr/data/15059468/openapi.do
- Station code mapping: [midterm.py](../scripts/midterm.py) `STATION_CODES` dictionary
- Code examples: [midterm.py](../scripts/midterm.py)


path: references/api-warnings.md
content: # Weather Warnings API Reference

KMA weather warning service provides official warnings and advisories for severe weather conditions.

**Base URL**: `https://apis.data.go.kr/1360000/WthrWrnInfoService`

**Service**: 기상청 기상특보 조회서비스 (15000415)

## Endpoint

### Get Warning Status

**Endpoint**: `/getPwnStatus`

**Description**: Get current nationwide weather warning status summary

**Parameters**:

| Parameter | Required | Type | Description | Example |
|-----------|----------|------|-------------|---------|
| serviceKey | Yes | String | API service key | (your key) |
| numOfRows | Yes | Integer | Number of rows per page | 10 |
| pageNo | Yes | Integer | Page number | 1 |
| dataType | Yes | String | Response format (XML/JSON) | JSON |

**Example Request**:
```bash
curl "https://apis.data.go.kr/1360000/WthrWrnInfoService/getPwnStatus?serviceKey=YOUR_KEY&numOfRows=10&pageNo=1&dataType=JSON"
```

**Response Fields**:

| Field | Description | Example |
|-------|-------------|---------|
| tmFc | Issue time (YYYYMMDDHHmm) | 202602011000 |
| tmEf | Effective time (YYYYMMDDHHmm) | 202602011000 |
| tmSeq | Sequence number | 3 |
| t6 | Current active warnings summary | o 건조경보 : 강원도... |
| t7 | Preliminary warnings | (1) 강풍 예비특보... |
| other | Other information | o 없음 |

**Usage Note**: This endpoint provides a concise nationwide summary of all active warnings and preliminary warnings, ideal for quick status checks.

---

## Warning Types

### Warning Levels

Korean weather warnings have two severity levels:

1. **Advisory (주의보)**: Caution required, prepare for potential hazards
2. **Warning (경보)**: Severe conditions, take protective actions immediately

### Warning Type Codes

| Code | Korean | English | Type |
|------|--------|---------|------|
| W0 | 강풍경보 | Strong Wind Warning | Warning |
| W1 | 호우경보 | Heavy Rain Warning | Warning |
| W2 | 한파경보 | Cold Wave Warning | Warning |
| W3 | 건조경보 | Dry Weather Warning | Warning |
| W4 | 폭풍해일경보 | Storm Surge Warning | Warning |
| W5 | 풍랑경보 | High Waves Warning | Warning |
| W6 | 태풍경보 | Typhoon Warning | Warning |
| W7 | 대설경보 | Heavy Snow Warning | Warning |
| W8 | 황사경보 | Yellow Dust Warning | Warning |
| W9 | 폭염경보 | Heat Wave Warning | Warning |
| O0 | 강풍주의보 | Strong Wind Advisory | Advisory |
| O1 | 호우주의보 | Heavy Rain Advisory | Advisory |
| O2 | 한파주의보 | Cold Wave Advisory | Advisory |
| O3 | 건조주의보 | Dry Weather Advisory | Advisory |
| O4 | 폭풍해일주의보 | Storm Surge Advisory | Advisory |
| O5 | 풍랑주의보 | High Waves Advisory | Advisory |
| O6 | 태풍주의보 | Typhoon Advisory | Advisory |
| O7 | 대설주의보 | Heavy Snow Advisory | Advisory |
| O8 | 황사주의보 | Yellow Dust Advisory | Advisory |
| O9 | 폭염주의보 | Heat Wave Advisory | Advisory |

---

## Warning Criteria

### Heavy Rain (호우)

**Advisory (주의보)**:
- 3-hour rainfall ≥ 60mm
- 12-hour rainfall ≥ 110mm

**Warning (경보)**:
- 3-hour rainfall ≥ 90mm
- 12-hour rainfall ≥ 180mm

### Heavy Snow (대설)

**Advisory (주의보)**:
- 24-hour snowfall ≥ 5cm (plains/coast)
- 24-hour snowfall ≥ 20cm (mountainous areas)

**Warning (경보)**:
- 24-hour snowfall ≥ 20cm (plains/coast)
- 24-hour snowfall ≥ 30cm (mountainous areas)

### Strong Wind (강풍)

**Advisory (주의보)**:
- Wind speed ≥ 14 m/s (plains)
- Wind speed ≥ 17 m/s (mountains)
- Wind speed ≥ 21 m/s (coastal areas)

**Warning (경보)**:
- Wind speed ≥ 21 m/s (plains)
- Wind speed ≥ 24 m/s (mountains)
- Wind speed ≥ 26 m/s (coastal areas)

### Cold Wave (한파)

**Advisory (주의보)**:
- Morning minimum temperature ≤ -12°C (2 consecutive days)
- Morning minimum drops ≥ 10°C in 24 hours to ≤ 3°C

**Warning (경보)**:
- Morning minimum temperature ≤ -15°C (2 consecutive days)
- Morning minimum drops ≥ 15°C in 24 hours to ≤ 3°C

### Heat Wave (폭염)

**Advisory (주의보)**:
- Daily maximum temperature ≥ 33°C (2 consecutive days)

**Warning (경보)**:
- Daily maximum temperature ≥ 35°C (2 consecutive days)

### Typhoon (태풍)

**Advisory (주의보)**:
- Typhoon expected to affect region within 48 hours
- Wind speed expected to reach 17-24 m/s

**Warning (경보)**:
- Typhoon expected to affect region within 24 hours
- Wind speed expected to exceed 25 m/s

---

## Response Format

**JSON Structure**:
```json
{
  "response": {
    "header": {
      "resultCode": "00",
      "resultMsg": "NORMAL_SERVICE"
    },
    "body": {
      "dataType": "JSON",
      "items": {
        "item": [
          {
            "stnId": "108",
            "title": "서울특별시 호우주의보",
            "tmFc": "202602011200",
            "tmSeq": "1",
            "wrnId": "O1"
          },
          ...
        ]
      },
      "pageNo": 1,
      "numOfRows": 10,
      "totalCount": 3
    }
  }
}
```

**Key Fields**:

| Field | Description | Example |
|-------|-------------|---------|
| stnId | Station/Region ID | 108 (Seoul) |
| title | Warning title | 서울특별시 호우주의보 |
| tmFc | Issue time (YYYYMMDDHHmm) | 202602011200 |
| tmSeq | Sequence number | 1 |
| wrnId | Warning type code | O1 (Heavy Rain Advisory) |

---

## Station IDs

Common station IDs for major cities:

| Station ID | Region | Korean Name |
|------------|--------|-------------|
| 108 | Seoul | 서울 |
| 112 | Incheon | 인천 |
| 119 | Suwon | 수원 |
| 131 | Gangneung | 강릉 |
| 133 | Daejeon | 대전 |
| 143 | Daegu | 대구 |
| 146 | Jeonju | 전주 |
| 152 | Ulsan | 울산 |
| 156 | Gwangju | 광주 |
| 159 | Busan | 부산 |
| 184 | Jeju | 제주 |
| 0 | All stations | 전국 |

---

## Usage Notes

### Time Range

- Use `fromTmFc` and `toTmFc` to specify time range
- Recommended: Query last 24-48 hours for current warnings
- Format: YYYYMMDDHHmm (e.g., 202602011200 = 2026-02-01 12:00)

### Station Filtering

- Use `stnId=0` to get warnings for all regions (recommended)
- Use specific station ID to filter by region
- A single warning may affect multiple stations

### Active Warnings

To get currently active warnings:
```python
from datetime import datetime, timedelta

# Query last 24 hours
to_time = datetime.now().strftime("%Y%m%d%H%M")
from_time = (datetime.now() - timedelta(days=1)).strftime("%Y%m%d%H%M")

fetch_warnings(from_time, to_time, stn_id="0")
```

### Warning Lifecycle

1. **Issue**: Warning is first issued (주의보/경보 발표)
2. **Update**: Warning is updated with new information (갱신)
3. **Upgrade**: Advisory upgraded to Warning (경보로 상향)
4. **Downgrade**: Warning downgraded to Advisory (주의보로 하향)
5. **Cancel**: Warning is cancelled (해제)

Check the `title` field for status keywords: 발표, 갱신, 상향, 하향, 해제

---

## Error Codes

See [api-forecast.md](api-forecast.md#error-codes) for common error codes.

---

## Integration Example

**Check for warnings and display**:
```python
from warnings import fetch_warnings, format_warnings

# Get recent warnings
data = fetch_warnings()

# Check if there are active warnings
items = data["response"]["body"]["items"]["item"]

if items:
    print("⚠️  Active weather warnings!")
    print(format_warnings(data))
else:
    print("✅ No active weather warnings")
```

**Filter by warning type**:
```python
items = data["response"]["body"]["items"]["item"]

# Filter for typhoon warnings
typhoon_warnings = [
    item for item in items
    if item.get("wrnId") in ["O6", "W6"]
]

if typhoon_warnings:
    print("🌀 Typhoon warning in effect!")
```

---

## Best Practices

1. **Check regularly**: Query every 1-3 hours during severe weather season
2. **Use stnId=0**: Get warnings for all regions to avoid missing any
3. **Parse title**: Extract status (발표/해제) from title field
4. **Alert users**: Display warnings prominently when detected
5. **Store history**: Keep track of warning lifecycle for analytics

---

## References

- Official API page: https://www.data.go.kr/data/15000415/openapi.do
- KMA warning criteria: https://www.kma.go.kr/
- Code examples: [weather_warnings.py](../scripts/weather_warnings.py)


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