# malgleam (맑을림)
[](https://hex.pm/packages/malgleam)
[](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 문서 생성
```