ADMap
v1.2.0SDK 시작 →
02 · SDK

SDK 연동

ADMap 은 QGIS 로 저작한 미디어 플래닝 지도를 외부 컨슈머에게 제공하기 위해 두 가지 SDK 진입점을 제공한다. 둘 다 1.2.0 기준이며 fixed versioning 으로 항상 같은 버전을 함께 올린다.

진입점 고르기

◆

일반 브라우저 앱

@team-draftype/map-sdk — MapLibre 기반 지도 임베드(AdMap)와 REST helper(ApiClient)

◆

React 앱

@team-draftype/react — 선언형 <ADMap> 컴포넌트 + 카탈로그 read 훅 + 컨슈머 셀프서브 레이어 write 훅(useConsumerLayers) + InfoWindow·브랜드 대시보드·모바일 시트 컴포넌트

이 SDK 는 발행된(published) 카탈로그 read + 컨슈머 소유 레이어 write 만 노출한다. 임의 맵 확장(addLayer/addSource/getMapInstance)은 안정 API 가 아니다.

설치

두 패키지 모두 private GitHub Packages 레지스트리에 게시된다. 프로젝트 루트 .npmrc 에 레지스트리와 토큰을 설정한다. 토큰은 read:packages 스코프의 PAT 이면 된다.

# .npmrc
@team-draftype:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

@team-draftype/react 와 @team-draftype/map-sdk 는 fixed versioning 이다. 둘을 함께 쓸 때는 반드시 같은 버전(1.2.0)으로 고정한다.

인증 — X-API-Key (중요)

운영자가 발급한 admap_<...> 키를 모든 요청에 X-API-Key 헤더로 싣는다. 하나의 키로 SDK REST 호출·직접 API 호출·style 이 참조하는 tile/glyph/sprite 요청을 전부 인증한다.

키 전달 방식은 두 가지다.

✓

same-origin 프록시 (권장)

소비팀 서버가 허용된 map/brand 범위로 프록시하며 서버사이드에서 X-API-Key 를 주입한다. 브라우저 코드는 키를 보유하지 않는다.

◎

순수 프론트엔드 (SDK-only)

프록시 없이 브라우저에서 origin-locked 키를 직접 싣는다. 키의 origin lock 이 도난 재사용을 막는다.

직접 API 를 호출할 때는 헤더로 싣는다.

ts
const res = await fetch('<BASE>/api/v1/maps', {
  headers: { 'X-API-Key': 'admap_xxxxxxxx', Accept: 'application/json' },
});

SDK 에서는 키를 SDK 옵션으로 넘기면 REST + tile/glyph/sprite 요청에 자동 주입된다 (vanilla AdMap({ apiKey }) · React <ADMap apiKey>). read 훅에는 별도로 headers: { 'X-API-Key': ... } 를 전달한다(아래 각 절 참조).

style 요청 규약 (ADM-150): apiKey 를 지정하면 style(resourceType: Style) 요청에는 URL host 매칭과 무관하게 항상 X-API-Key 가 부착된다 — 절대 baseUrl 을 쓰는 cross-origin 컨슈머도 style 이 인증된다. tile/glyph/sprite 는 style 이 참조하는 URL 을 그대로 따른다.

사용량 식별 헤더: SDK 는 모든 /api/v1/* 요청에 X-Admap-Client: react 또는 map-sdk 를 자동으로 싣는다. authz 축이 아니라 로깅/분석 표시용 힌트로, ADMap 이 SDK 트래픽과 직접 API 트래픽을 나눠 집계하는 데 쓴다. same-origin 프록시 컨슈머는 이 헤더를 forward 해야 SDK 호출로 집계된다(안 하면 직접 API 로 잡힘).

지도 임베드

두 진입점 모두 같은 지도를 임베드한다 — vanilla 는 AdMap, React 는 <ADMap>.

import { AdMap } from '@team-draftype/map-sdk';
import 'maplibre-gl/dist/maplibre-gl.css';

const map = new AdMap({
apiBase: '<BASE>/api/v1',
mapId: 'main',
container: '#map',
apiKey: 'admap_xxxxxxxx', // 순수 프론트엔드만; same-origin 프록시면 생략
theme: 'light',           // 'light' | 'dark' | 'minimal' | 'satellite'
brand: 'downy',           // 선택. 미설정 시 서버가 'main' 으로 정규화
center: [126.978, 37.566],
zoom: 12,
initialViewMode: '2d',    // '2d' | '3d'
});

// 클릭한 피처 수신 — logicalLayerId 는 카탈로그(논리) 레이어, layerId 는 실제 style 레이어
const off = map.on('feature-click', ({ layerId, logicalLayerId, feature, lngLat }) => {
console.log(logicalLayerId, layerId, feature.properties, lngLat);
});

map.setViewMode('3d');                    // pitch easeTo + 건물 extrusion 자동 적용
map.setTheme('dark');
await map.setBrand('mediheal');           // 403(brand_not_allowed) 시 이전 brand 로 silent rollback
const brands = await map.listBrands();    // 이 키가 쓸 수 있는 brand 목록
map.setLayerVisibility('poi_cafe', false);          // 논리 id — main + companion 함께 토글
map.setFilter('poi_cafe', ['==', 'category', 'cafe']); // family 전체, baseline 과 AND 합성
map.setLayerOpacity('poi_cafe', 0.4);               // 파생(__casing/__cluster/__3d)까지 일괄
map.flyTo({ center: [127.02, 37.5], zoom: 14 });

off();          // 리스너 해제
map.destroy();  // 정리

AdMap 공개 메서드: setViewMode / getViewMode · setTheme · setBrand / getBrand / listBrands · getLayerTaxonomy · setLayerVisibility · setFilter · setLayerOpacity · flyTo · on(→ 해제 함수 반환) · destroy. 이벤트: load · error · feature-click(payload: layerId · logicalLayerId · feature · lngLat).

3D 건물: setViewMode('3d') 시 metadata['admap:extrude']===true 로 태깅된 사용자 fill 레이어가 {id}__3d fill-extrusion 으로 파생되고, 자체 데이터 커버리지 밖은 OSM 건물이 fallback 으로 주입된다(osmBuildings 옵션, 기본 true). 저수준 제어가 필요하면 패키지 top-level export applyUserBuildingExtrusion · setOsmBuildingsVisibility · ensureOsmBuildingSource 를 쓴다 (AdMap 인스턴스 메서드가 아니라 standalone 함수).

런타임 레이어 제어 (논리 레이어 family)

컴파일 파이프라인은 카탈로그 레이어 하나를 여러 style 레이어로 emit 한다 — companion split(__casing / __outline / __text / __cluster)과 QML/IR per-symbol sublayer(__sN / __c<key>__sN / __rN__sN). setLayerVisibility · setFilter · setLayerOpacity 는 모두 논리(카탈로그) id 하나를 받아 그 family 전체를 다룬다. 파생 id 는 문서화된 계약이 아니므로 직접 지정하지 않는다.

setLayerOpacity / useLayerOpacity, setLayerVisibility 의 companion 일괄 토글 · feature-click 의 logicalLayerId · setFilter 의 family AND 합성은 1.2.0 에서 도입됐다. 1.1.0 이하에서는 setFilter / setLayerVisibility 가 main style 레이어에만 적용됐다.

◐

setLayerVisibility(id, visible)

논리 id 로 호출하면 main 과 companion 이 함께 숨거나 나타난다. (fill 을 숨겼는데 테두리만 남는 문제 해소.)

⁝

setFilter(id, filter | null)

family 전체에 적용하되 각 style 레이어의 authored filter 를 baseline 으로 보존하고 ['all', baseline, filter] 로 AND 합성한다(non-stacking). null 은 baseline 복원.

◔

setLayerOpacity(id, opacity)

[0,1] clamp 후 파생(__casing / __cluster / __3d)까지 일괄 곱. __outline 테두리와 __text 라벨은 곱 대상 제외 — authored 값 유지.

baseline 보존이 중요한 이유: companion 의 authored filter 는 사용자 filter 가 아니라 main 과의 상보 분할(main: ['!', ['has','point_count']] vs __cluster: ['has','point_count'])이거나 v2 category 술어다. 덮어쓰면 클러스터링이 반전되거나 카테고리 분할이 사라진다. setFilter 는 매 호출을 현재값이 아니라 baseline 에서 다시 합성하고, 브랜드 전환·재발행·setStyle({diff:true}) 후 styledata 에서 새 authored 를 baseline 으로 재수집한다. 미존재 논리 id 는 세 메서드 모두 no-op. 식별은 발행 style 의 metadata['admap:layerId'] 를 우선 읽고, 없으면 id suffix collapse 폴백이라 이 키 없이 발행된 옛 style 도 동일하게 동작한다.

REST helper: ApiClient

ts
import { ApiClient, ApiClientError } from '@team-draftype/map-sdk';

const api = new ApiClient('<BASE>/api/v1', {
  headers: { 'X-API-Key': 'admap_xxxxxxxx' }, // 순수 프론트엔드만
});

try {
  const { maps } = await api.listMaps();
  const style = await api.getStyle('main', 'light', 'downy'); // MapLibre style spec v8
  const { layers } = await api.listLayers('main');
  const taxonomy = await api.getLayerTaxonomy('main');
  const { sprites } = await api.listIcons();
  const { icons } = await api.getSprite(sprites[0].id);
} catch (err) {
  if (err instanceof ApiClientError && err.isRateLimit) {
    // 429 — 백오프 후 재시도 (SDK 는 재시도하지 않는다)
  }
}

ApiClient read: listMaps · getStyle(mapId, theme, brand?) · listLayers(mapId) · getLayerTaxonomy(mapId) · listIcons · getSprite(spriteId). ApiClientError 는 status 와 isRateLimit(429) · isAuthError(401/403) · isNotFound(404) 접근자를 제공한다. 컨슈머 셀프서브 write 메서드는 아래 절에서 다룬다.

React 카탈로그 read 훅

세 훅 모두 { apiBase, mapId(또는 spriteId), headers?, fetchImpl? } 를 받고 { ..., loading, error, refetch } 를 돌려준다. <ADMap apiKey> 와 달리 read 훅은 인증이 필요하면 headers 로 키를 직접 전달한다.

tsx
import { useAvailableBrands, useLayerTaxonomy, useIcons } from '@team-draftype/react';

const API = '<BASE>/api/v1';
const auth = { headers: { 'X-API-Key': 'admap_xxxxxxxx' } }; // 순수 프론트엔드만

function Sidebar() {
  const { brands } = useAvailableBrands({ apiBase: API, mapId: 'main', ...auth });
  const { taxonomy } = useLayerTaxonomy({ apiBase: API, mapId: 'main', ...auth });
  const { sprites } = useIcons({ apiBase: API, ...auth });

  return (
    <ul>
      {brands.map((b) => <li key={b.id}>{b.name}</li>)}
      {taxonomy.segments.map((c) => <li key={c.id}>{c.name}</li>)}
      {sprites.map((s) => <li key={s.id}>{s.name} ({s.iconCount})</li>)}
    </ul>
  );
}

맵 컨텍스트 훅 · 컴포넌트

<ADMap> 하위에서 쓰는 훅:

| 훅 | 시그니처 | 용도 | | --- | --- | --- | | useADMap() | → { map, style, layerRegistry } | provider 의 MapLibre 인스턴스·style 접근 | | useMapEvent(event, handler) | | MapLibre 맵 이벤트 구독(자동 해제) | | useMapLayer(entry) | entry: MapLayerSpec | source+layer 선언형 추가, style upgrade 시 자동 복원 | | useLayerOpacity(layerId, opacity) | | 논리 레이어 opacity multiplier 선언형 적용(family 일괄) | | useClusterClick() | | 클러스터 클릭 시 자동 줌인 | | useElementGroup(group) | → ElementGroupState | 맵 위 오버레이 element 그룹 상태 공유 | | useFeatureSelection(map, selected) | | 선택 피처의 feature-state 동기화 |

컴포넌트: <InfoWindow> / ConfiguredTemplate(클릭 인포창) · <BrandDashboardTabs>(브랜드 미니 대시보드) · <MobileBottomSheet>(모바일 바텀시트) · <FeatureList> / <MapFeatureSync>(뷰포트 피처 리스트 동기화, useViewportFeatures) · <ViewModeToggle>(2D/3D 토글 버튼).

buildStyleUrl(mapId, options) (react top-level export) — Config API style URL 을 조립한다. options.baseUrl 이 절대 URL(https://…)이면 origin 을 보존한 완전 URL을, 상대 기본값 (/api/v1)이면 path+search 를 반환한다(하위호환). theme · brand · channel · version 을 query 로 싣는다. <ADMap> 대신 직접 MapLibre style URL 을 만들어야 할 때 쓴다.

컨슈머 셀프서브 레이어 (1.0.0)

컨슈머가 발급받은 api-key 소유로 GeoJSON 레이어를 직접 업로드·관리한다. 소유는 api-key 단위라 다른 컨슈머의 레이어와 격리된다(공용 authored 레이어는 전원 노출, 내 레이어는 내 키만). 업로드한 레이어는 다음 style fetch 부터 지도에 병합돼 렌더된다.

React: useConsumerLayers

tsx
import { useConsumerLayers } from '@team-draftype/react';

function LayerManager() {
  const { create, list, remove, pending, error } = useConsumerLayers({
    apiBase: '<BASE>/api/v1',
    mapId: 'main',
    headers: { 'X-API-Key': 'admap_xxxxxxxx' }, // 순수 프론트엔드만; 프록시면 생략
  });

  async function upload(file: File) {
    // GeoJSON File/Blob 또는 순수 객체 모두 허용
    const layer = await create({ file, name: 'my-pois', category: 'point_poi' });
    const { layers } = await list();
    console.log(layer.layerId, layers.length);
  }

  return (
    <>
      <input type="file" accept=".geojson,.json" onChange={(e) => e.target.files && upload(e.target.files[0])} />
      {pending && <span>uploading…</span>}
      {error && <span>{error.message}</span>}
    </>
  );
}

useConsumerLayers(...) 는 { create, list, get, rename, setVisibility, update, remove, pending, error } 를 돌려준다.

Vanilla: ApiClient

ts
const layer = await api.createConsumerLayer('main', {
  file: geojson,          // Blob/File 또는 GeoJSON 객체
  name: 'my-pois',
  category: 'point_poi',
});
const { layers } = await api.listConsumerLayers('main');

vanilla 는 createConsumerLayer · listConsumerLayers · getConsumerLayer · renameConsumerLayer · setConsumerLayerVisibility(zoom 범위) · updateConsumerLayer(데이터 교체) · deleteConsumerLayer 로 동일 기능을 쓴다.

1.0.0 GA 이전 — 제거된 표면

이전 버전에서 다음 표면이 제거됐다. 참조가 있으면 걷어낸다.

REMOVED이전 버전에서 제거된 표면 — 참조가 있으면 걷어낸다
  • 통합검색 — React /react/search 서브패스와 search 컴포넌트 전부 제거.
  • taxonomy groups / consumerType — layer taxonomy 는 이제 segments(대분류 → 소분류 → layerIds) 트리만 반환한다.
  • Impact / three.js 오버레이(5.0.0) — 임팩트 shockwave·카메라·three-impact 레이어 제거.
  • 공간분석 · routing — 관련 엔드포인트·헬퍼 제거.
  • category 값 재편 — building / poi / zone → polygon_poi / point_poi 로 통합.
  • 3D 플래그 — 3D 는 별도 API 가 아니라 style layer 의 metadata['admap:extrude']===true 플래그로 표현된다(setViewMode('3d') / <ADMap viewMode="3d"> 가 이를 파생).

운영 규약

  • API key 는 운영자가 발급하며 하나의 키로 SDK·직접 API·tile/glyph/sprite 를 인증한다. same-origin 프록시로 서버 주입하거나(권장), 순수 프론트엔드면 origin-locked 키를 headers/ apiKey 로 직접 싣는다.
  • SDK 는 공개 카탈로그 read + 컨슈머 셀프서브 레이어 write 를 제공한다. addLayer·addSource· getMapInstance 같은 임의 맵 확장 API 는 안정 표면이 아니다.
  • ApiClient/훅은 재시도/백오프를 하지 않는다. 429 는 컨슈머가 명시적으로 백오프한다.
  • 세부 요청/응답 스키마는 API 레퍼런스에서 확인한다.

다음 단계

  • REST 만 연동하면 빠른 시작을 먼저 따른다.
  • 지도 임베드가 필요하면 @team-draftype/map-sdk 를 기본 선택으로 둔다.
  • React 화면에 바로 붙일 때만 @team-draftype/react 를 선택한다.