lustre_kakaomap

Package Version Hex Docs

Gleam / Lustre 애플리케이션을 위한 선언적 카카오맵 래퍼 라이브러리.

gleam add lustre_kakaomap@1

시작하기

1. 카카오맵 SDK 로드

API 키는 카카오 개발자사이트에서 JavaScript 키를 발급받으세요. 키를 소스에 직접 노출하지 않도록 .env 파일로 관리하는 것을 권장합니다.

# .env
KAKAO_APP_KEY=발급받은_APP_KEY

HTML 파일에서는 플레이스홀더를 사용하고, dev 서버가 .env의 값을 주입합니다:

<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%KAKAO_APP_KEY%"></script>

서비스(장소 검색, 주소 변환)나 클러스터러를 사용하려면 라이브러리를 함께 로드합니다:

<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%KAKAO_APP_KEY%&libraries=services,clusterer,drawing"></script>

example/ 디렉토리에 .env.exampledev.mjs(환경 변수 주입 dev 서버)가 포함되어 있습니다. cp .env.example .env 후 키를 입력하고 node dev.mjs로 실행하세요.

2. Lustre 앱에서 사용

import lustre/effect
import lustre/element/html
import lustre_kakaomap as kakao
import lustre_kakaomap/coords
import lustre_kakaomap/events
import lustre_kakaomap/marker

type Msg {
  MapClicked(coords.LatLng)
  MapIdle
}

// View - 지도 컨테이너 렌더링
fn view(model) {
  html.div([], [
    kakao.map(id: "mymap", attributes: []),
  ])
}

// Init - 지도 초기화 + 이벤트 구독
fn init(_) {
  #(model, effect.batch([
    kakao.init(id: "mymap", options: [
      kakao.center(coords.seoul()),
      kakao.level(3),
    ]),
    events.on_click(id: "mymap", handler: MapClicked),
    events.on_idle(id: "mymap", handler: fn() { MapIdle }),
  ]))
}

// Update - 클릭 시 마커 추가
fn update(model, msg) {
  case msg {
    MapClicked(pos) -> #(
      model,
      marker.add(id: "mymap", marker_id: "m1", options: [
        marker.position(pos),
        marker.title("클릭!"),
      ]),
    )
    MapIdle -> #(model, effect.none())
  }
}

모듈 목록

핵심 (Stage 1)

모듈설명
lustre_kakaomap지도 초기화, 뷰 렌더링, 지도 제어 + 상태 조회 (Getter)
lustre_kakaomap/coordsLatLng, LatLngBounds, Point, Size + 한국 도시 프리셋 + 거리/방위각 계산
lustre_kakaomap/typesMapTypeId, ControlPosition, StrokeStyle 열거형
lustre_kakaomap/events지도, 마커, 도형 이벤트 구독 + 개별 이벤트 해제 (listen/off)
lustre_kakaomap/marker마커 오버레이 + 선언적 마커 동기화 (marker.sync)
lustre_kakaomap/info_window인포윈도우 팝업 + 타입 안전 콘텐츠 + 선언적 동기화
lustre_kakaomap/custom_overlayHTML 기반 커스텀 오버레이 + 타입 안전 콘텐츠 + 선언적 동기화
lustre_kakaomap/preset조합 가능한 Map 프리셋 시스템 (파이프라인 API)

도형 (Stage 2)

모든 도형 모듈은 파이프라인 빌더 API(from |> color |> fill |> draw)와 clear() 함수를 제공합니다.

모듈설명
lustre_kakaomap/polyline폴리라인 (파이프라인 API)
lustre_kakaomap/polygon다각형 (파이프라인 API)
lustre_kakaomap/circle원 (파이프라인 API)
lustre_kakaomap/ellipse타원 (파이프라인 API)
lustre_kakaomap/rectangle사각형 (파이프라인 API)

고급 기능 (Stage 3)

모듈설명필요 라이브러리
lustre_kakaomap/roadview로드뷰 (스트리트뷰) + 상태 조회 Getter기본 SDK
lustre_kakaomap/services/places장소 검색 (키워드, 카테고리) + 타입 안전 CategoryCodeservices
lustre_kakaomap/services/geocoder주소<->좌표 변환services
lustre_kakaomap/clusterer마커 클러스터링 + 이벤트 확장clusterer

유틸리티 (Stage 4)

모듈설명필요 라이브러리
lustre_kakaomap/static_map정적 지도 (비인터랙티브 이미지)기본 SDK
lustre_kakaomap/drawing그리기 도구 + 데이터 추출 + undo/redo 상태drawing
lustre_kakaomap/geojsonGeoJSON 좌표 변환 헬퍼 (순수 Gleam, FFI 불필요)없음
lustre_kakaomap/url카카오맵 URL 빌더 (순수 Gleam, FFI 불필요)없음

프리셋 시스템

자주 쓰는 지도 설정을 파이프라인으로 조합합니다.

import lustre_kakaomap/preset
import lustre_kakaomap/coords

// 읽기 전용 지도 (임베드용)
preset.readonly()
|> preset.with_center(coords.jeju())
|> preset.with_level(5)
|> preset.apply(id: "embed_map")

// 사용 가능한 프리셋:
preset.clean_map()      // 기본 로드맵
preset.satellite()      // 위성 뷰
preset.hybrid()         // 하이브리드 (위성 + 도로명)
preset.readonly()       // 읽기 전용 (드래그/줌 비활성화)
preset.full_control()   // 모든 인터랙션 활성화

파이프라인 친화적 도형 API

모든 도형 모듈은 from |> color |> fill |> draw 파이프라인 패턴을 지원합니다.

import lustre_kakaomap/polyline
import lustre_kakaomap/circle
import lustre_kakaomap/coords
import lustre_kakaomap/types

// 폴리라인 - 경로선
polyline.from([coords.seoul(), coords.daejeon(), coords.busan()])
|> polyline.color("#FF0000")
|> polyline.weight(3)
|> polyline.style(types.Dash)
|> polyline.opacity(0.8)
|> polyline.arrow()
|> polyline.draw(id: "mymap", shape_id: "route")

// 원 - 반경 표시
circle.from(coords.seoul(), radius: 5000.0)
|> circle.color("#3366FF")
|> circle.fill(color: "#CFE7FF", opacity: 0.3)
|> circle.draw(id: "mymap", shape_id: "area")

// 모든 도형 한번에 제거
circle.clear(id: "mymap")

Map 상태 조회 (Getter)

지도의 현재 상태를 콜백으로 읽어올 수 있습니다.

import lustre_kakaomap as kakao

type Msg {
  GotCenter(coords.LatLng)
  GotLevel(Int)
  GotBounds(coords.LatLngBounds)
  GotMapType(types.MapTypeId)
}

// update 함수에서:
kakao.get_center(id: "mymap", handler: GotCenter)
kakao.get_level(id: "mymap", handler: GotLevel)
kakao.get_bounds(id: "mymap", handler: GotBounds)
kakao.get_map_type(id: "mymap", handler: GotMapType)

타입 안전한 오버레이 콘텐츠

Lustre Element를 직접 오버레이 콘텐츠로 사용할 수 있습니다. 원시 HTML 문자열 대신 타입 안전한 마크업을 제공합니다.

import lustre/element/html
import lustre/element
import lustre/attribute
import lustre_kakaomap/info_window
import lustre_kakaomap/custom_overlay

// 기존 방식 (raw HTML 문자열)
info_window.content("<div style='padding:5px'>Hello</div>")

// 타입 안전한 방식 (Lustre Element)
info_window.content_element(
  html.div([attribute.style("padding", "5px")], [
    element.text("Hello"),
  ])
)

// custom_overlay에도 동일하게 사용
custom_overlay.content_element(
  html.div([attribute.style("background", "#fff")], [
    element.text("Kakao HQ"),
  ])
)

이벤트 구독 해제

Named listener로 이벤트를 구독하고 개별적으로 해제할 수 있습니다.

import lustre_kakaomap/events

// Named listener로 이벤트 구독
events.listen(
  id: "mymap", event: "click",
  listener_id: "my_click",
  handler: MapClicked,
)

// 상태 이벤트용
events.listen_simple(
  id: "mymap", event: "idle",
  listener_id: "my_idle",
  handler: fn() { MapIdle },
)

// 개별 리스너 해제
events.off(id: "mymap", listener_id: "my_click")

// 전체 named 리스너 해제
events.off_all(id: "mymap")

선언적 동기화

모델의 리스트를 지도와 자동 동기화합니다. 새 항목 추가, 사라진 항목 제거, 변경된 항목 업데이트를 자동 처리합니다.

import lustre_kakaomap/marker
import lustre_kakaomap/info_window
import lustre_kakaomap/custom_overlay
import lustre_kakaomap/coords

// 마커 동기화
marker.sync(id: "mymap", markers: [
  #("seoul", coords.seoul(), "서울시청"),
  #("busan", coords.busan(), "부산역"),
])

// 인포윈도우 동기화
info_window.sync(id: "mymap", info_windows: [
  #("iw1", "<div>서울</div>", coords.seoul()),
  #("iw2", "<div>부산</div>", coords.busan()),
])

// 커스텀 오버레이 동기화
custom_overlay.sync(id: "mymap", overlays: [
  #("label1", "<div>Seoul</div>", coords.seoul()),
])

좌표 유틸리티

import lustre_kakaomap/coords

// 한국 도시 프리셋
coords.seoul()      // 서울시청
coords.busan()      // 부산역
coords.pangyo()     // 판교 카카오 본사
// + daegu, incheon, gwangju, daejeon, ulsan, sejong, jeju

// 거리 계산 (Haversine 공식, 미터 단위)
let distance = coords.distance(from: coords.seoul(), to: coords.busan())
// 약 325,000.0 (325km)

// 방위각 계산 (도 단위, 0-360)
let bearing = coords.bearing(from: coords.seoul(), to: coords.busan())
// 약 165도 (남남동)

// 목적지 좌표 계산 (방위각 + 거리)
let dest = coords.destination(from: coords.seoul(), bearing_deg: 90.0, distance_m: 1000.0)
// 서울에서 동쪽으로 1km 지점

// 좌표 오프셋
let shifted = coords.offset(coords.seoul(), lat_offset: 0.01, lng_offset: -0.01)

// Bounds 유틸리티
let bounds = coords.bounds_from_list([coords.seoul(), coords.busan(), coords.jeju()])
let center = coords.bounds_center(bounds)
let overlaps = coords.bounds_overlap(bounds_a, bounds_b)

GeoJSON 좌표 변환

import lustre_kakaomap/geojson
import lustre_kakaomap/polyline

// GeoJSON은 [경도, 위도] 순서 - 이 모듈이 자동으로 LatLng(위도, 경도)로 변환
let path = geojson.line_coords([
  #(126.978, 37.566),   // 서울 (경도, 위도)
  #(129.042, 35.114),   // 부산
])

// 바로 폴리라인으로 그리기
geojson.draw_line_string(path,
  id: "mymap", shape_id: "route",
  options: [polyline.stroke_color("#FF0000")],
)

URL 빌더 (FFI 불필요)

import lustre_kakaomap/url
import lustre_kakaomap/coords

// 지도 바로가기 링크
url.map_link_named(name: "서울시청", position: coords.seoul())
// -> "https://map.kakao.com/link/map/서울시청,37.5665,126.978"

// 자동차 경로 링크
let from = url.named_location(name: "서울역", position: coords.seoul())
let to = url.named_location(name: "부산역", position: coords.busan())
url.route_by(mode: url.Car, from:, to:)

// 지하철 경로
url.subway_route(region: url.SeoulSubway, from: "판교역", to: "강남역")

// 검색 결과
url.search(query: "강남 카페")

장소 검색

import lustre_kakaomap/services/places
import lustre_kakaomap/services/status

// 키워드 검색 (SDK에 &libraries=services 필요)
places.keyword_search(
  keyword: "강남 맛집",
  options: [places.size(15), places.sort(status.Accuracy)],
  handler: fn(search_status, results) { GotPlaces(search_status, results) },
)

// 타입 안전 카테고리 검색 (문자열 코드 대신 CategoryCode 사용)
places.category_search_by(
  category: places.Cafe,
  options: [places.location(coords.seoul()), places.radius(1000)],
  handler: fn(search_status, results) { GotCafes(search_status, results) },
)

17개 카테고리 코드를 타입으로 제공합니다: Mart, ConvStore, School, Academy, Parking, GasStation, Subway, Bank, Culture, Brokerage, PublicInst, Attraction, Lodge, Restaurant, Cafe, Hospital, Pharmacy

주소<->좌표 변환

import lustre_kakaomap/services/geocoder

// 주소 -> 좌표
geocoder.address_search(
  address: "서울시 강남구 역삼동",
  handler: fn(status, results) { GotAddress(status, results) },
)

// 좌표 -> 주소
geocoder.coord2_address(
  position: coords.seoul(),
  handler: fn(status, addr, road_addr) { GotReverseGeocode(status, addr, road_addr) },
)

마커 클러스터링

import lustre_kakaomap/clusterer

// 클러스터러 초기화 (SDK에 &libraries=clusterer 필요)
clusterer.init(id: "mymap", clusterer_id: "c1", options: [
  clusterer.grid_size(60),
  clusterer.min_cluster_size(2),
])

// 마커를 클러스터러에 추가
clusterer.add_marker(id: "mymap", clusterer_id: "c1", marker_id: "m1")

// 클러스터 이벤트
clusterer.on_cluster_click(id: "mymap", clusterer_id: "c1", handler: fn(pos) { ClusterClicked(pos) })
clusterer.on_cluster_over(id: "mymap", clusterer_id: "c1", handler: fn(pos) { ClusterHovered(pos) })

로드뷰

import lustre_kakaomap/roadview

// 로드뷰 초기화
roadview.init(id: "rv", options: [
  roadview.init_pan(0.0),
  roadview.init_tilt(0.0),
])

// 가장 가까운 파노라마 찾기
roadview.get_nearest_pano_id(
  position: coords.seoul(),
  radius: 50.0,
  handler: fn(pano_id) { GotPanoId(pano_id) },
)

// 현재 상태 조회
roadview.get_pano_id(id: "rv", handler: fn(pid) { GotCurrentPano(pid) })
roadview.get_viewpoint_state(id: "rv", handler: fn(vp) { GotViewpoint(vp) })
roadview.get_position(id: "rv", handler: fn(pos) { GotRvPosition(pos) })

그리기 도구

import lustre_kakaomap/drawing

// 그리기 매니저 초기화 (SDK에 &libraries=drawing 필요)
drawing.init(
  map_id: "mymap",
  drawing_id: "draw1",
  modes: [drawing.DrawPolyline, drawing.DrawPolygon, drawing.DrawCircle],
  options: [drawing.stroke_color("#FF0000"), drawing.removable(True)],
)

// 그리기 모드 선택
drawing.select(map_id: "mymap", drawing_id: "draw1", mode: drawing.DrawPolygon)

// 그려진 데이터 추출 (JSON)
drawing.get_data(map_id: "mymap", drawing_id: "draw1", handler: fn(json) { GotDrawingData(json) })

// Undo/Redo 가능 여부 확인
drawing.get_undoable(map_id: "mymap", drawing_id: "draw1", handler: fn(can) { CanUndo(can) })
drawing.get_redoable(map_id: "mymap", drawing_id: "draw1", handler: fn(can) { CanRedo(can) })

자세한 API 문서는 https://hexdocs.pm/lustre_kakaomap에서 확인할 수 있습니다.

개발

gleam deps download   # 의존성 다운로드
gleam build           # 빌드
gleam test            # 테스트 실행
gleam format          # 코드 포맷팅
gleam docs build      # 문서 생성

라이선스

MIT

Search Document