본문으로 건너뛰기
연동 인터페이스 · 서비스 개발자를 위한 안내

편집 창 연동하기

서비스 페이지에서 편집 창을 열고, 편집할 데이터를 보내고, 편집 결과를 받는 방법입니다. 편집기 주소와 메시지 순서를 확인한 뒤 사각형 하나로 연결해 보세요.

편집기 주소와 연결 조건

서비스 페이지는 편집 창을 열고 결과를 받을 웹페이지입니다. 편집 창은 아래 주소를 새 창으로 연 화면입니다. 데이터는 브라우저의 postMessage로 두 창 사이에서 주고받습니다.

https://maps-editor.pages.dev/editor/
  • 다른 HTTPS 도메인에서도 연결할 수 있습니다. 서비스 페이지에 자바스크립트를 추가할 수 있어야 하며, 공개 편집기의 기본 설정에서는 별도 도메인 등록이 필요하지 않습니다. 지도 라이브러리 없이도 연결할 수 있습니다.

  • 사용자 클릭으로 편집 창을 열고 서비스 페이지는 결과를 받을 때까지 유지하세요. 주소에 데이터를 붙이는 대신, 준비 완료 알림을 받은 뒤 메시지로 보냅니다.

  • http://localhost:3000 같은 HTTP 개발 주소에서 공개 편집기를 사용하려면 편집기 운영자의 허용이 필요합니다. 서비스 페이지와 편집기를 같은 프로토콜·도메인·포트에서 실행하는 로컬 환경은 기본 설정으로 연결할 수 있습니다.

창 연결과 주소 확인의 상세 조건

사이트 주소(origin)는 프로토콜·도메인·포트의 조합이며 경로는 포함하지 않습니다. 예를 들어 https://service.example/orders의 사이트 주소는 https://service.example입니다. 편집기 운영자가 허용 목록을 설정한 배포에서는 HTTPS도 그 목록을 따릅니다.

두 창의 연결을 유지하기 위해 noopener·noreferrer를 사용하지 마세요. 창 연결을 끊는 보안 정책도 확인해야 합니다. iframe·file://·null origin은 지원하지 않습니다.

주소만 직접 열면 편집 데이터를 기다립니다

서비스 페이지에서 편집 창을 열고 데이터를 보내야 편집을 시작할 수 있습니다. 먼저 동작을 확인하려면 문서 안의 실습을 사용하세요.

두 창이 데이터를 주고받는 순서

편집 창 메시지 순서
단계방향메시지와 데이터
1. 준비 완료 알림편집 창 → 서비스 페이지MAP_EDITOR_READY · 데이터를 받을 준비가 되었음을 알림
2. 편집할 데이터 보내기서비스 페이지 → 편집 창MAP_EDITOR_INIT · 편집 작업 ID(sessionId)와 편집 데이터(scene)
3. 편집 결과 보내기편집 창 → 서비스 페이지MAP_EDITOR_SUBMIT · 같은 sessionId와 최종 scene 전체
취소한 경우편집 창 → 서비스 페이지MAP_EDITOR_CANCEL · 같은 sessionId, 편집 결과 없음
입력 오류가 있는 경우편집 창 → 서비스 페이지MAP_EDITOR_ERROR · 오류 설명(message), 선택적 상세 정보(issues)

편집 작업 ID(sessionId)는 서비스 페이지가 이번 편집을 구분하기 위해 만드는 고유한 문자열입니다. 로그인 토큰이 아닙니다. 결과의 작업 ID를 확인한 뒤 편집 창을 닫고 결과를 반영합니다. 취소한 경우에는 기존 데이터를 유지합니다.

사각형 하나를 보내고 결과 JSON 받기

아래 예제는 지도 라이브러리 없이 버튼과 결과 표시 영역만 사용합니다. 편집 완료 시 onSubmit에 결과가 들어오고, 다음 편집에는 방금 받은 도형을 다시 보냅니다.

  • TypeScript를 빌드하는 서비스 프로젝트에서 npm install zod를 실행합니다. 아래 최소 예제와 접힌 두 보조 파일을 같은 폴더에 저장하세요.

  • 서비스 페이지에 아래 HTML을 추가하고, 요소가 만들어진 뒤 연결 코드를 실행합니다. 편집기 자체의 설치나 별도 서버 구성은 필요하지 않습니다.

서비스 페이지의 HTML
<button id="edit" type="button">도형 편집하기</button><pre id="result"></pre>
minimal-service.example.ts
import type { EditorSceneInput } from "./editor-contract.example";import { createMapEditorHost } from "./map-editor-host.example";
// 서비스 페이지의 버튼과 결과 표시 영역을 연결합니다.export function mountMinimalExample(  button: HTMLButtonElement,  result: HTMLElement,  editorUrl = "https://maps-editor.pages.dev/editor/",) {  let scene: EditorSceneInput = {    version: 2,    features: [      {        id: "area-1",        name: "예제 도형",        geometry: {          type: "Polygon",          coordinates: [            [              [126.974, 37.576],              [126.98, 37.576],              [126.98, 37.582],              [126.974, 37.582],              [126.974, 37.576],            ],          ],        },      },    ],  };  const editor = createMapEditorHost({    editorUrl,    getScene: () => scene,    onSubmit(editedScene) {      scene = editedScene; // 다음 편집은 이번 편집 결과로 시작합니다.      result.textContent = JSON.stringify(scene, null, 2);      // 서버에 저장하려면 여기서 서비스의 저장 기능을 호출합니다.    },    onCancel() {      result.textContent = "편집을 취소했습니다. 기존 편집 데이터를 유지합니다.";    },    onError(message) {      result.textContent = message;    },  });  const openEditor = () => {    try {      editor.open();    } catch (error) {      result.textContent =        error instanceof Error ? error.message : "편집 창을 열지 못했습니다.";    }  };  button.addEventListener("click", openEditor);  return () => {    // 서비스 페이지를 떠나거나 이 화면을 제거할 때 실행합니다.    button.removeEventListener("click", openEditor);    editor.dispose();  };}
서비스 페이지에서 실행할 연결 코드
import { mountMinimalExample } from "./minimal-service.example";
const button = document.querySelector<HTMLButtonElement>("#edit");const result = document.querySelector<HTMLElement>("#result");if (button && result) {  const cleanup = mountMinimalExample(button, result);  window.addEventListener("pagehide", (event) => {    if (!event.persisted) cleanup();  });}

React 등에서는 화면이 사라질 때 반환된 정리 함수 cleanup()을 실행하세요. 취소하면 결과 반영 함수를 호출하지 않습니다.createMapEditorHost는 이 문서에서 복사해 쓰는 보조 함수이며, 별도 배포된 SDK는 아닙니다. 창과 사이트 주소, 편집 작업 ID 및 결과 형식을 검사합니다.

필수 보조 파일 · 창 연결과 메시지 처리
map-editor-host.example.ts
import {  completionMessageSchema,  type EditorSceneInput,} from "./editor-contract.example";
const EDITOR_WINDOW_NAME = "map-editor-child";const EDITOR_WINDOW_FEATURES = "width=1280,height=860";
type MapEditorHostOptions = {  editorUrl: string;  getScene: () => EditorSceneInput;  onSubmit: (scene: EditorSceneInput) => void;  onCancel?: () => void;  onError?: (message: string) => void;};
export function createMapEditorHost(options: MapEditorHostOptions) {  const editorUrl = new URL(options.editorUrl, window.location.href);  const editorOrigin = editorUrl.origin;  let editorWindow: Window | null = null;  let sessionId: string | null = null;  let initialScene: EditorSceneInput | null = null;
  const closeEditor = () => {    editorWindow?.close();    editorWindow = null;    sessionId = null;    initialScene = null;  };
  const handleMessage = (event: MessageEvent<unknown>) => {    // 반드시 내가 연 창과 배포된 편집기의 정확한 origin을 함께 확인합니다.    const targetWindow = editorWindow;    if (      !targetWindow ||      event.source !== targetWindow ||      event.origin !== editorOrigin    ) {      return;    }
    const data = event.data;    if (      typeof data !== "object" ||      data === null ||      !("type" in data) ||      typeof data.type !== "string"    ) {      return;    }
    if (data.type === "MAP_EDITOR_READY") {      // 같은 팝업의 재전송에도 세션과 최초 입력을 일관되게 유지합니다.      if (!sessionId || !initialScene) return;      targetWindow.postMessage(        {          type: "MAP_EDITOR_INIT",          sessionId,          scene: initialScene,        },        editorOrigin,      );      return;    }
    const completion = completionMessageSchema.safeParse(data);    if (completion.success) {      if (completion.data.sessionId !== sessionId) {        return;      }
      closeEditor();      if (completion.data.type === "MAP_EDITOR_SUBMIT") {        options.onSubmit(completion.data.scene);      } else {        options.onCancel?.();      }      return;    }
    if (data.type === "MAP_EDITOR_ERROR") {      const message =        "message" in data && typeof data.message === "string"          ? data.message          : "지도 편집기에서 오류가 발생했습니다.";      options.onError?.(message);    }  };
  window.addEventListener("message", handleMessage);
  return {    open() {      // 중복 클릭으로 이미 편집 중인 창을 닫거나 데이터를 덮지 않습니다.      if (editorWindow && !editorWindow.closed) {        editorWindow.focus();        return;      }      closeEditor();      initialScene = structuredClone(options.getScene());      sessionId = crypto.randomUUID();      editorWindow = window.open(        editorUrl.href,        EDITOR_WINDOW_NAME,        EDITOR_WINDOW_FEATURES,      );
      if (!editorWindow) {        closeEditor();        throw new Error("팝업이 차단되었습니다.");      }    },    dispose() {      window.removeEventListener("message", handleMessage);      closeEditor();    },  };}
필수 보조 파일 · 편집 결과 검증과 타입
editor-contract.example.ts
import { z } from "zod";
// 외부 서비스로 복사할 수 있는 최소 검증 예제입니다. 업무별 저장 권한·면적 검증은 별도입니다.const coordinateSchema = z.tuple([  z.number().min(-180).max(180),  z.number().min(-90).max(90),]);const lineStringCoordinatesSchema = z.array(coordinateSchema).min(2);const ringSchema = z  .array(coordinateSchema)  .min(3)  .superRefine((ring, context) => {    const first = ring[0];    const last = ring[ring.length - 1];    const closed = first && last && first[0] === last[0] && first[1] === last[1];    const vertices = closed ? ring.slice(0, -1) : ring;    if (      (closed && ring.length < 4) ||      new Set(vertices.map((point) => point.join(","))).size < 3    ) {      context.addIssue({        code: "custom",        message: "Polygon에는 서로 다른 정점이 3개 이상 필요합니다.",      });    }  });const polygonCoordinatesSchema = z.array(ringSchema).min(1);
const geometrySchema = z.discriminatedUnion("type", [  z.object({    type: z.literal("Point"),    coordinates: coordinateSchema,  }),  z.object({    type: z.literal("MultiPoint"),    coordinates: z.array(coordinateSchema).min(1),  }),  z.object({    type: z.literal("LineString"),    coordinates: lineStringCoordinatesSchema,  }),  z.object({    type: z.literal("MultiLineString"),    coordinates: z.array(lineStringCoordinatesSchema).min(1),  }),  z.object({    type: z.literal("Polygon"),    coordinates: polygonCoordinatesSchema,  }),  z.object({    type: z.literal("MultiPolygon"),    coordinates: z.array(polygonCoordinatesSchema).min(1),  }),]);
const featureInputSchema = z.object({  geometry: geometrySchema,  id: z.string().optional(),  name: z.string().optional(),  locked: z.boolean().optional(),  visible: z.boolean().optional(),  themeToken: z.string().optional(),  properties: z.record(z.string(), z.unknown()).optional(),});
export const editorSceneInputSchema = z.object({  version: z.literal(2),  features: z.array(featureInputSchema),  id: z.string().optional(),  name: z.string().optional(),  viewport: z    .object({      center: coordinateSchema.optional(),      zoom: z.number().optional(),    })    .optional(),});
export const completionMessageSchema = z.discriminatedUnion("type", [  z.object({    type: z.literal("MAP_EDITOR_SUBMIT"),    sessionId: z.string().min(1),    scene: editorSceneInputSchema,  }),  z.object({    type: z.literal("MAP_EDITOR_CANCEL"),    sessionId: z.string().min(1),  }),]);
export type EditorSceneInput = z.infer<typeof editorSceneInputSchema>;

보내는 편집 데이터의 형식

편집 데이터(scene)는 version: 2와 도형 목록(features)을 담습니다. 각 도형에 형태와 좌표 (geometry)를 넣으세요. 빈 목록{ version: 2, features: [] }으로 새 도형을 그리기부터 시작할 수도 있습니다.

편집 데이터 필드
필드뜻조건
version편집 데이터 형식의 버전필수 · 2
features도형 목록필수 · 빈 배열 허용
features[].geometry도형의 형태와 좌표필수 · GeoJSON geometry
features[].id · name도형 식별자와 표시 이름선택 · id 생략 시 생성, 제공한 id는 중복 불가
features[].properties서비스에서 함께 보낸 추가 정보선택 · 예: { deliveryAreaId: 'area-1' }
features[].locked · visible · themeToken잠금·표시·스타일선택 · 잠금은 사용자가 해제 가능
scene.id · name · viewport편집 데이터 전체의 식별자·이름·지도 설정선택 · 현재 viewport는 결과에 보존되지만 지도 위치에 적용되지 않음

좌표는 WGS84 [경도, 위도] 순서입니다. 지도 객체 대신 좌표 데이터를 보내며, GeoJSON FeatureCollection 전체를 바로 보내는 형식은 아닙니다. 다각형(Polygon)은 첫 점과 마지막 점을 같게 해서 닫으세요.

여러 도형을 보내는 예제와 추가 조건
input-scene.example.ts · 다각형·선·점
import type { EditorSceneInput } from "./editor-contract.example";
// 경복궁 주변의 권역·경로·위치를 표현하는 연동 연습용 데이터입니다.// 실제 지적·행정 경계나 공식 관람 동선이 아닙니다.export const inputScene = {  version: 2, // 필수: 입출력 데이터 형식의 버전  name: "경복궁 권역 편집", // 선택: 편집 데이터 전체의 이름. 편집 결과에도 포함  features: [    // 필수: 표시할 도형 목록. []이면 빈 지도, 배열 뒤쪽 도형이 위에 표시    {      id: "gyeongbokgung-area", // 선택: 반환된 도형을 찾는 고유 ID. 생략 시 자동 생성      name: "경복궁 예제 권역", // 선택: 도형 목록과 지도 라벨에 표시할 이름      locked: false, // 선택, 기본 false: 편집 가능. true는 잠금 시작, UI에서 해제 가능      visible: true, // 선택, 기본 true: 지도 표시 여부. 숨겨도 편집 결과에 포함      // 필수: 실제 위치와 모양을 담는 GeoJSON geometry      geometry: {        type: "Polygon", // 면적을 가진 다각형        // WGS84 [경도, 위도]. 첫 링은 바깥 경계, 추가 링은 내부에서 비울 구멍        coordinates: [          [            [126.972, 37.5745], // 남서            [126.982, 37.5745], // 남동            [126.982, 37.5855], // 북동            [126.972, 37.5855], // 북서            [126.972, 37.5745], // 첫 점으로 돌아와 링 닫기          ],        ],      },      // 선택: 좌표를 편집해 저장해도 함께 돌려받는 업무 정보      properties: { serviceAreaId: "palace-001" }, // 서비스가 정한 필드명과 권역 ID    },    {      id: "gyeongbokgung-route",      name: "경복궁 예제 경로",      locked: false,      visible: true,      geometry: {        type: "LineString", // 선: 두 개 이상의 좌표를 순서대로 연결        coordinates: [          [126.976, 37.577], // 시작점: 아래 마커와 같은 위치          [126.976, 37.5795], // 경유점          [126.979, 37.5815], // 도착점. 폴리곤처럼 첫 점으로 닫지 않습니다.        ],      },      properties: { routeId: "palace-route-001" }, // 서비스에서 경로를 식별할 업무 ID    },    {      id: "gyeongbokgung-marker",      name: "경복궁 예제 마커",      locked: false,      visible: true,      geometry: {        type: "Point", // 마커: 좌표 목록이 아닌 [경도, 위도] 한 쌍        coordinates: [126.976, 37.577], // 예제 경로의 시작 위치      },      properties: { placeId: "palace-marker-001" }, // 서비스에서 장소를 식별할 업무 ID    },  ],} satisfies EditorSceneInput;

Point/MultiPoint/LineString/MultiLineString/Polygon/MultiPolygon을 지원합니다. GeometryCollection은 지원하지 않습니다. 좌표 범위는 경도 −180~180, 위도 −90~90이며, 다른 좌표계를 사용하면 변환이 필요합니다. 다각형은 서로 다른 정점이 최소 3개 있어야 합니다. 도형 목록의 뒤쪽 도형이 지도 위쪽에 표시되며, 결과도 이 순서를 유지합니다.

받은 편집 결과를 서비스에 반영하기

MAP_EDITOR_SUBMIT의 scene에는 최종 도형 목록 전체가 들어갑니다. 받은 편집 결과로 서비스 페이지가 보관한 데이터를 교체하세요. 변경된 도형만 추가하는 방식으로 처리하면 삭제된 도형이 남거나 중복될 수 있습니다.

편집 결과 처리
편집 창에서 한 일서비스 페이지가 받는 결과
도형 수정수정된 형태와 좌표. 기존 도형의 식별자와 추가 정보는 함께 반환
도형 삭제·숨김삭제된 도형은 목록에서 제외. 숨긴 도형은 visible: false로 포함
도형 그리기·참고 경계 추가새 식별자의 도형 포함. 서비스의 업무 ID는 필요하면 서비스에서 연결
참고 경계 조회조회만 한 참고 경계는 제외. 내 도형에 추가하거나 합친 결과만 포함
편집 취소취소 알림만 수신. 편집 데이터가 없으므로 기존 데이터를 유지

편집 완료와 서버 저장을 구분하세요

편집 창의 ‘저장하고 편집 완료’ 버튼은 결과를 보내는 동작입니다.onSubmit에서 화면을 갱신하고, 필요하면 자신의 저장 API를 호출하세요. 저장 실패 표시와 재시도는 서비스에서 처리합니다.편집 완료 조건

지도 화면에 결과 반영하기 · bindMapEditor
서비스 페이지 연결 함수
함수서비스에서 연결할 일
getScene()편집할 도형 목록과 설정 읽기
applyScene(scene)보관 중인 편집 데이터를 받은 결과 전체로 교체하기
renderOnMap(featureCollection)받은 좌표를 사용하는 지도 라이브러리로 변환해 표시하기
showEditorError(message)오류 설명을 화면에 표시하기
service-page.example.ts
import type { EditorSceneInput } from "./editor-contract.example";import { createMapEditorHost } from "./map-editor-host.example";
type ServicePageBindings = {  // 예: https://maps-editor.pages.dev/editor/ 또는 로컬 /editor/  editorUrl: string;  openButton: HTMLButtonElement;  // 창을 열 때 서비스의 현재 데이터를 읽습니다. 입력 예시는 input-scene.example.ts 참고.  getScene: () => EditorSceneInput;  // 결과 전체를 다음 편집의 기준 데이터로 보관합니다.  applyScene: (scene: EditorSceneInput) => void;  // 사용하는 지도 라이브러리에 맞춰 이 GeoJSON을 반영합니다.  renderOnMap: (featureCollection: {    type: "FeatureCollection";    features: Array<{      type: "Feature";      id?: string;      geometry: EditorSceneInput["features"][number]["geometry"];      properties: Record<string, unknown>;    }>;  }) => void;  showEditorError: (message: string) => void;};
export function bindMapEditor({  editorUrl,  openButton,  getScene,  applyScene,  renderOnMap,  showEditorError,}: ServicePageBindings) {  const mapEditor = createMapEditorHost({    editorUrl,    getScene,    onSubmit(editedScene) {      // applyScene에서 getScene이 읽는 상태를 갱신하면 다음 편집도 수정본으로 시작합니다.      applyScene(editedScene);      // 서비스 지도의 읽기/갱신 API는 지도 라이브러리에 맞게 연결합니다.      renderOnMap({        type: "FeatureCollection",        features: editedScene.features.map((feature) => ({          type: "Feature",          id: feature.id,          geometry: feature.geometry,          properties: {            ...feature.properties,            name: feature.name,            locked: feature.locked,            visible: feature.visible,          },        })),      });    },    onCancel() {      // CANCEL에는 scene이 없습니다. applyScene을 호출하지 않아 기존 데이터를 유지합니다.    },    onError: showEditorError,  });
  const openEditor = () => {    try {      mapEditor.open();    } catch (error) {      showEditorError(        error instanceof Error ? error.message : "편집기를 열지 못했습니다.",      );    }  };  openButton.addEventListener("click", openEditor);
  return () => {    openButton.removeEventListener("click", openEditor);    mapEditor.dispose();  };}

설치 없이 편집 결과를 받아보는 실습

이 문서가 서비스 페이지 역할을 합니다. 편집 창에 경복궁 주변의 다각형·선·점을 보내고, 편집을 완료하면 받은 결과를 아래 지도와 JSON에 반영합니다. 도구 사용법은 편집 화면 안내에서 확인하세요.

서비스 페이지의 예제 지도경복궁 권역·경로·위치 마커 · 편집 결과를 이곳에 반영합니다

폴리곤·선·마커가 각각 1개씩 준비되었습니다.

사각형은 권역, 선은 예제 경로, 마커는 경로의 시작 위치입니다. 실제 지적·행정 경계나 공식 관람 동선은 아닙니다. 처음 열린 편집 창에서 경복궁 주변을 확대해 보세요. 이 예제는 서버에 저장하지 않으며 새로고침하면 초기화됩니다.

편집 창에 보낼 데이터 JSON

각 필드의 목적을 주석으로 적은 JSONC 예제입니다. ‘JSON 복사’를 누르면 주석 없는 실제 데이터를 복사합니다. 편집 창에는 이 데이터를 INIT 메시지의 scene 객체로 보냅니다.

현재 입력.jsonc · 필드 설명
{  // 필수: 편집 창과 주고받는 데이터 형식의 버전입니다. 현재는 2를 사용합니다.  "version": 2,  // 선택: 편집 데이터 전체의 이름입니다. 편집 결과에도 포함됩니다.  "name": "경복궁 권역 편집",  // 필수: 새 창에 표시할 도형 목록입니다. []이면 도형 없이 시작합니다.  // 여러 도형을 넣으면 배열 뒤쪽 도형이 지도 위에 겹쳐 그려집니다.  "features": [    {      // 선택: 저장 후 같은 도형을 찾기 위한 고유 ID입니다.      // 생략하면 자동 생성됩니다. 같은 목록 안에서 중복될 수 없습니다.      "id": "gyeongbokgung-area",      // 선택: 도형 목록과 지도 라벨에 표시할 이름입니다.      "name": "경복궁 예제 권역",      // 선택, 기본 false: false는 편집 가능, true는 잠금 상태로 시작합니다.      // 사용자가 화면에서 잠금을 해제할 수 있으므로 접근 권한 설정은 아닙니다.      "locked": false,      // 선택, 기본 true: 지도에 표시할지 정합니다.      // false로 숨겨도 도형 데이터는 편집 결과에 포함됩니다.      "visible": true,      // 필수: 실제 위치와 모양을 나타내는 GeoJSON geometry입니다.      "geometry": {        // Polygon은 면적을 가진 다각형입니다.        "type": "Polygon",        // WGS84 [경도, 위도] 순서입니다. 첫 번째 링은 바깥 경계이며,        // 뒤에 링을 추가하면 다각형 내부에서 비워 둘 구멍이 됩니다.        "coordinates": [          [            [126.972, 37.5745], // 남서쪽 꼭짓점            [126.982, 37.5745], // 남동쪽 꼭짓점            [126.982, 37.5855], // 북동쪽 꼭짓점            [126.972, 37.5855], // 북서쪽 꼭짓점            [126.972, 37.5745], // 첫 좌표를 반복해 바깥 경계를 닫습니다.          ],        ],      },      // 선택: 서비스에서 쓰는 업무 정보를 도형에 붙여 보관합니다.      // 이 도형의 좌표를 편집해 저장해도 함께 돌려받습니다.      "properties": {        // 예제 서비스의 권역 ID입니다. 필드명과 값은 서비스에서 자유롭게 정합니다.        "serviceAreaId": "palace-001",      },    },    {      // 같은 도형 목록에 선을 추가합니다. 폴리곤 위에 그려집니다.      "id": "gyeongbokgung-route",      "name": "경복궁 예제 경로",      "locked": false,      "visible": true,      "geometry": {        // LineString은 두 개 이상의 좌표를 순서대로 잇는 선입니다.        "type": "LineString",        // Polygon의 링보다 배열이 한 단계 얕습니다. 첫 점으로 닫을 필요가 없습니다.        "coordinates": [          [126.976, 37.577], // 시작점: 아래 마커와 같은 위치          [126.976, 37.5795], // 경유점          [126.979, 37.5815], // 도착점: 실제 관람 동선이 아닌 연습용 경로입니다.        ],      },      "properties": {        // 좌표를 수정한 뒤에도 이 업무 ID로 서비스의 경로 데이터와 연결합니다.        "routeId": "palace-route-001",      },    },    {      // 마지막에 넣어 선과 폴리곤 위에 표시할 위치 마커입니다.      "id": "gyeongbokgung-marker",      "name": "경복궁 예제 마커",      "locked": false,      "visible": true,      "geometry": {        // Point는 지도에 핀 모양의 마커로 표시됩니다.        "type": "Point",        // 좌표 목록이 아닌 [경도, 위도] 한 쌍입니다. 예제 경로의 시작 위치입니다.        "coordinates": [126.976, 37.577],      },      "properties": {        // 서비스에서 장소를 식별하는 업무 ID입니다. 필드명은 자유롭게 정합니다.        "placeId": "palace-marker-001",      },    },  ],}
실습의 전송 메시지와 편집 결과 예시

아래 비교는 다각형의 북동쪽 정점 하나를 옮긴 예시입니다. 직접 편집한 결과는 위 실습의 JSON에 표시됩니다.

편집할 데이터를 보내는 메시지.json
{  "type": "MAP_EDITOR_INIT",  "sessionId": "edit-example-1",  "scene": {    "version": 2,    "name": "경복궁 권역 편집",    "features": [      {        "id": "gyeongbokgung-area",        "name": "경복궁 예제 권역",        "locked": false,        "visible": true,        "geometry": {          "type": "Polygon",          "coordinates": [            [              [                126.972,                37.5745              ],              [                126.982,                37.5745              ],              [                126.982,                37.5855              ],              [                126.972,                37.5855              ],              [                126.972,                37.5745              ]            ]          ]        },        "properties": {          "serviceAreaId": "palace-001"        }      },      {        "id": "gyeongbokgung-route",        "name": "경복궁 예제 경로",        "locked": false,        "visible": true,        "geometry": {          "type": "LineString",          "coordinates": [            [              126.976,              37.577            ],            [              126.976,              37.5795            ],            [              126.979,              37.5815            ]          ]        },        "properties": {          "routeId": "palace-route-001"        }      },      {        "id": "gyeongbokgung-marker",        "name": "경복궁 예제 마커",        "locked": false,        "visible": true,        "geometry": {          "type": "Point",          "coordinates": [            126.976,            37.577          ]        },        "properties": {          "placeId": "palace-marker-001"        }      }    ]  }}
편집 결과를 받는 메시지.json
{  "type": "MAP_EDITOR_SUBMIT",  "sessionId": "edit-example-1",  "scene": {    "version": 2,    "name": "경복궁 권역 편집",    "features": [      {        "id": "gyeongbokgung-area",        "name": "경복궁 예제 권역",        "locked": false,        "visible": true,        "geometry": {          "type": "Polygon",          "coordinates": [            [              [                126.972,                37.5745              ],              [                126.982,                37.5745              ],              [                126.984,                37.5865              ],              [                126.972,                37.5855              ],              [                126.972,                37.5745              ]            ]          ]        },        "properties": {          "serviceAreaId": "palace-001"        }      },      {        "id": "gyeongbokgung-route",        "name": "경복궁 예제 경로",        "locked": false,        "visible": true,        "geometry": {          "type": "LineString",          "coordinates": [            [              126.976,              37.577            ],            [              126.976,              37.5795            ],            [              126.979,              37.5815            ]          ]        },        "properties": {          "routeId": "palace-route-001"        }      },      {        "id": "gyeongbokgung-marker",        "name": "경복궁 예제 마커",        "locked": false,        "visible": true,        "geometry": {          "type": "Point",          "coordinates": [            126.976,            37.577          ]        },        "properties": {          "placeId": "palace-marker-001"        }      }    ]  }}

메시지 처리를 직접 구현할 때 확인할 조건

  • 메시지 수신기를 먼저 등록한 뒤 사용자 클릭에서 창을 엽니다. 메시지를 보낸 창(event.source)이 내가 연 편집 창인지, 사이트 주소(event.origin)가 정해 둔 편집기 주소와 맞는지 확인하세요. 완료·취소 알림은 편집 작업 ID와 데이터 구조도 검사합니다.

  • 데이터를 보낼 때는 받을 사이트 주소(targetOrigin)를 정확히 지정합니다. *는 쓰지 않습니다. 편집 데이터에는 로그인 토큰이나 비밀 키를 넣지 마세요.

  • 같은 편집 창이 준비 완료 알림을 다시 보내면 같은 편집 작업 ID와 처음 준비한 데이터로 응답합니다. 새 시작 메시지는 현재 편집과 되돌리기 기록을 초기화하므로 중간 데이터 갱신 용도로 반복해서 보내지 마세요.

  • 중간 변경 알림과 자동 저장은 제공하지 않습니다. 오류 설명은 안전한 텍스트로 표시하고, 서비스 서버에 저장할 때는 업무에 필요한 좌표·권한 검증을 수행하세요. 공개 편집기의 참고 경계 로그인은 편집 창에서 처리합니다.

연결이 안 될 때 확인하기

편집 창 연결 문제 해결
증상확인할 것
편집 창이 열리지 않음사용자 클릭에서 창을 여는지, 브라우저가 팝업을 차단했는지 확인
편집 데이터를 기다리는 중서비스 페이지의 버튼으로 열었는지, 수신기를 먼저 등록했는지, 두 창의 연결과 사이트 주소가 유지되는지 확인
보낸 도형이 화면에 안 보임목록에서 도형 이름 확인 후 지도 이동·확대. 현재 기본 중심 [126.98, 37.57], 줌 12이며 입력 범위로 자동 이동하지 않음
입력 오류version: 2, 편집 작업 ID, 도형 ID 중복, 경도·위도 순서와 데이터 형식 확인
결과를 받았지만 서버에 저장되지 않음onSubmit에서 서비스의 저장 API를 호출하는지 확인. 실패 표시와 재시도도 서비스에서 처리