README.md

# malgleam (맑을림)

[![Package Version](https://img.shields.io/hexpm/v/malgleam)](https://hex.pm/packages/malgleam)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/malgleam/)

기상청 Open API 3종의 타입 안전 Gleam 래퍼. Erlang/JavaScript 양쪽 타겟 지원.

```sh
gleam add malgleam@1
```

## 왜 malgleam인가?

기상청 API를 직접 호출하려면 격자 변환 공식, 카테고리 코드표, 지역번호 목록, XML/JSON 파싱을 모두 직접 처리해야 합니다. malgleam은 이 모든 번거로움을 타입 시스템 뒤에 숨깁니다.

| 직접 호출 시 | malgleam 사용 시 |
|-------------|-----------------|
| 서울 위경도를 Lambert Conformal Conic 공식으로 격자 변환 | `location.seoul` |
| `"T1H"`, `"PTY"`, `"SKY"` 코드표 참조 | `obs.temperature`, `obs.precipitation_type` |
| 하늘상태 `"1"` → 맑음 매핑 | `case obs { Clear -> ... }` |
| 중기육상예보 구역코드 `"11B00000"` 조회 | `region.land_seoul_incheon_gyeonggi` |
| 생활기상지수 행정구역코드 `"1100000000"` 조회 | `area.seoul` |
| URL 조립 + JSON 파싱 + 에러코드 처리 | 요청 빌더 + 디코더가 전부 처리 |

## 빠른 시작

### 서울의 현재 날씨

```gleam
import malgleam/short_term
import malgleam/location
import gleam/httpc

let request = short_term.ultra_srt_ncst(
  service_key: "my-service-key",
  location: location.seoul,
  base_date: "20240701",
  base_time: "0600",
)
let assert Ok(response) = httpc.send(request)
let assert Ok(obs) = short_term.decode_ultra_srt_ncst(response)

obs.temperature         // 24.5 (℃)
obs.humidity            // 78 (%)
obs.precipitation_type  // NoPrecipitation
obs.wind_direction      // W (16방위)
obs.wind_speed          // 2.1 (m/s)
```

8개 관측 카테고리(T1H, RN1, UUU, VVV, REH, PTY, VEC, WSD)가 **의미 있는 필드명의 단일 레코드**로 반환됩니다.

### 위경도로 위치 지정

```gleam
let busan = location.from_coords(lat: 35.1796, lng: 129.0756)
let request = short_term.vilage_fcst(
  service_key: "my-key",
  location: busan,
  base_date: "20240701",
  base_time: "0500",
)
```

Lambert Conformal Conic 격자 변환이 자동으로 처리됩니다.

### 단기예보 해석

```gleam
let assert Ok(items) = short_term.decode_vilage_fcst(response)

// 시간별 그룹핑
let groups = short_term.group_by_time(items)
// [#("20240701", "0600", [TMP항목, SKY항목, ...]),
//  #("20240701", "0900", [...]), ...]

// 개별 아이템 해석
short_term.parse_sky(sky_item)             // Ok(Clear)
short_term.parse_float_value(tmp_item)     // Ok(25.0)
short_term.parse_wind_direction(vec_item)  // Ok(S)
```

### 중기예보

```gleam
import malgleam/mid_term
import malgleam/region

let request = mid_term.mid_land_fcst(
  service_key: "my-key",
  region: region.land_seoul_incheon_gyeonggi,
  forecast_time: "202407010600",
)
let assert Ok(response) = httpc.send(request)
let assert Ok(forecast) = mid_term.decode_mid_land_fcst(response)

// days 4~7은 AM/PM, days 8~10은 하루 단위
list.each(forecast.days, fn(day) {
  case day.weather_am {
    Some(MidClear) -> "오전 맑음"
    Some(MidOvercastWithRain) -> "오전 비"
    _ -> "..."
  }
})
```

지역 코드를 외울 필요가 없습니다. 잘못된 코드 타입은 **컴파일 타임에 차단**됩니다.

```gleam
// OK: mid_land_fcst에 LandRegionId 전달
mid_term.mid_land_fcst(key, region.land_seoul_incheon_gyeonggi, time)

// 컴파일 에러: SeaRegionId는 mid_land_fcst에 전달 불가
mid_term.mid_land_fcst(key, region.sea_west_central, time)
```

### 생활기상지수

```gleam
import malgleam/living_index
import malgleam/area

let request = living_index.uv_idx(
  service_key: "my-key",
  area: area.seoul,
  time: "2024070618",
)
let assert Ok(response) = httpc.send(request)
let assert Ok(uv) = living_index.decode_uv_idx(response)

uv.tomorrow  // Some(8) — 내일 자외선지수 매우높음
```

시간별 지수 조회:

```gleam
let assert Ok(freeze) = living_index.decode_freeze_idx(response)
living_index.find_hourly(freeze.hourly, 6)  // Some(75) — 6시간 후 동파 위험 높음
```

### 선택 파라미터는 파이프라인으로

```gleam
let request = short_term.vilage_fcst(key, location.seoul, "20240701", "0500")
  |> short_term.with_rows(100)
  |> short_term.with_page(2)
```

## 지원 엔드포인트 (12개)

### 단기예보 (`malgleam/short_term`)

| 함수 | API | 설명 | 반환 타입 |
|------|-----|------|----------|
| `ultra_srt_ncst` | getUltraSrtNcst | 초단기실황 | `Observation` |
| `ultra_srt_fcst` | getUltraSrtFcst | 초단기예보 | `List(ForecastItem)` |
| `vilage_fcst` | getVilageFcst | 단기예보 | `List(ForecastItem)` |
| `fcst_version` | getFcstVersion | 예보버전 | `List(ForecastVersion)` |

### 중기예보 (`malgleam/mid_term`)

| 함수 | API | 설명 | 반환 타입 |
|------|-----|------|----------|
| `mid_fcst` | getMidFcst | 중기전망 | `MidOutlook` |
| `mid_land_fcst` | getMidLandFcst | 중기육상예보 | `MidLandForecast` |
| `mid_ta` | getMidTa | 중기기온 | `MidTempForecast` |
| `mid_sea_fcst` | getMidSeaFcst | 중기해상예보 | `MidSeaForecast` |

### 생활기상지수 (`malgleam/living_index`)

| 함수 | API | 설명 | 반환 타입 |
|------|-----|------|----------|
| `freeze_idx` | getFreezeIdxV2 | 동파가능지수 | `FreezeIndex` |
| `uv_idx` | getUVIdxV2 | 자외선지수 | `UvIndex` |
| `air_diffusion_idx` | getAirDiffusionIdxV2 | 대기확산지수 | `AirDiffusionIndex` |
| `sen_ta_idx` | getSenTaIdxV2 | 체감온도 | `SensibleTemperature` |

## 설계 원칙

### HTTP 클라이언트 독립

모든 요청 빌더는 `gleam/http/request.Request(String)`를 반환합니다. HTTP 클라이언트를 자유롭게 선택할 수 있습니다.

- Erlang: `gleam_httpc`
- JavaScript: `gleam_fetch`

### Result 기반 에러 처리

모든 디코더는 `Result(T, ApiError)`를 반환합니다.

```gleam
case short_term.decode_vilage_fcst(response) {
  Ok(items) -> // 성공
  Error(ApiServiceError(InvalidRequestParameter, msg)) -> // API 에러
  Error(JsonDecodeError(msg)) -> // JSON 파싱 실패
  Error(UnexpectedResponse(body)) -> // 예상치 못한 응답
}
```

기상청 API 에러코드(01~99) 전체가 `ErrorCode` variant로 매핑되어 있습니다.

### 주요 도시 상수

위치, 지역, 지점 코드를 상수로 제공합니다.

```gleam
// 단기예보 — 18개 도시 격자좌표
location.seoul  location.busan  location.daegu  location.jeju ...

// 중기예보 — 지점번호, 육상/해상/기온 구역코드
region.seoul_incheon_gyeonggi       // StationId
region.land_seoul_incheon_gyeonggi  // LandRegionId
region.temp_seoul                   // TempRegionId
region.sea_west_central             // SeaRegionId

// 생활기상지수 — 17개 시도 행정구역코드
area.seoul  area.busan  area.gyeonggi  area.jeju ...
```

## Development

```sh
gleam build        # 빌드
gleam test         # 테스트
gleam format       # 포맷
gleam docs build   # API 문서 생성
```