Skip to Content
Documentation선언형 API

선언형 API (defineElement)

dmn.plugin.defineElement는 복잡한 DOM 조작 없이 선언적으로 플러그인 UI를 정의할 수 있는 API입니다.

핵심 장점

기능설명
자동 설정 UI설정 스키마만 정의하면 다이얼로그가 자동 생성
인스턴스별 상태 격리같은 플러그인으로 여러 패널 생성 가능
탭(모드)별 격리생성한 탭에서만 패널이 표시됨
자동 상태 동기화메인 ↔ 오버레이 간 상태 자동 동기화
컨텍스트 메뉴 통합우클릭 메뉴 자동 생성
라이프사이클 관리탭 전환 시 자동 마운트/언마운트
자유로운 리사이즈그리드에서 직접 크기를 조절 가능

기본 구조

// @id my-plugin dmn.plugin.defineElement({ // 플러그인 이름 (컨텍스트 메뉴에 표시) name: "내 플러그인", // 최대 인스턴스 개수 (선택사항, 0 = 무제한) maxInstances: 1, // 그리드에서 크기 조절 가능 여부 (선택사항, 기본값: false) resizable: true, // 설정 변경 시 유지할 축 (선택사항, resizable이 true일 때만 적용, 기본값: "both") // "width" | "height" | "both" | "none" preserveAxis: "both", // 리사이즈 앵커 (선택사항, 기본값: "top-left") resizeAnchor: "center", // 컨텍스트 메뉴 설정 contextMenu: { create: "패널 생성", delete: "패널 삭제", items: [ /* 커스텀 메뉴 항목 */ ], }, // 설정 스키마 (자동 UI 생성) settings: { /* 설정 정의 */ }, // 다국어 메시지 messages: { /* 번역 데이터 */ }, // 메인 윈도우 미리보기 상태 previewState: { /* 미리보기용 초기 상태 */ }, // HTML 템플릿 함수 template: (state, settings, helpers) => { /* 템플릿 반환 */ }, // 오버레이 마운트 로직 onMount: (context) => { /* 초기화 로직 */ return () => { /* 클린업 */ }; }, });

설정 스키마 (settings)

설정 스키마를 정의하면 자동으로 설정 다이얼로그가 생성됩니다.

지원 타입

settings: { // 문자열 nickname: { type: "string", default: "", label: "닉네임", placeholder: "입력하세요", // 선택사항 }, // 숫자 fontSize: { type: "number", default: 14, label: "폰트 크기", min: 8, // 선택사항 max: 48, // 선택사항 step: 2, // 선택사항 }, // 불리언 (토글) showGraph: { type: "boolean", default: true, label: "그래프 표시", }, // 색상 textColor: { type: "color", default: "#FFFFFF", label: "텍스트 색상", }, // 선택 (드롭다운) theme: { type: "select", default: "dark", label: "테마", options: [ { label: "다크", value: "dark" }, { label: "라이트", value: "light" }, ], }, },

설정 접근

// template에서 template: (state, settings, { html }) => html` <div style="color: ${settings.textColor};"> ${settings.showGraph ? html`<div class="graph">...</div>` : ""} </div> `, // onMount에서 onMount: ({ getSettings }) => { const settings = getSettings(); console.log("현재 설정:", settings); },

리사이즈 앵커 (resizeAnchor)

요소의 크기가 변경될 때 기준이 되는 위치를 설정합니다. 기본값은 "top-left"입니다.

지원하는 앵커 위치 (9방향)

설명
top-left좌상단 (기본값)
top-center상단 중앙
top-right우상단
center-left좌측 중앙
center정중앙
center-right우측 중앙
bottom-left좌하단
bottom-center하단 중앙
bottom-right우하단

앵커 동작 설명

  • top-left: 크기가 변해도 좌상단 모서리가 고정됩니다 (기본 동작)
  • center: 크기가 변해도 중앙 위치가 고정됩니다
  • bottom-right: 크기가 변해도 우하단 모서리가 고정됩니다

Definition 레벨 설정

모든 인스턴스에 적용될 기본 앵커를 정의합니다:

dmn.plugin.defineElement({ name: "Centered Panel", resizeAnchor: "center", // 모든 인스턴스의 기본 앵커 // ... });

동적 앵커 변경 (onMount)

onMount에서 setAnchor()getAnchor()로 동적으로 앵커를 변경할 수 있습니다:

dmn.plugin.defineElement({ name: "Dynamic Anchor Panel", onMount: ({ setAnchor, getAnchor }) => { // 현재 앵커 확인 console.log("현재 앵커:", getAnchor()); // "top-left" // 앵커를 중앙으로 변경 setAnchor("center"); // 조건에 따라 앵커 변경 const settings = getSettings(); if (settings.expandFromCenter) { setAnchor("center"); } else { setAnchor("top-left"); } }, });

앵커 우선순위

  1. 인스턴스별 resizeAnchor (setAnchor로 설정)
  2. Definition의 resizeAnchor
  3. 기본값 "top-left"

리사이즈 설정 (resizable, preserveAxis)

플러그인 요소의 크기를 사용자가 그리드에서 직접 조절할 수 있도록 하려면 resizable 옵션을 사용합니다.

resizable

resizable: true로 설정하면 요소에 8방향 리사이즈 핸들이 표시됩니다.

dmn.plugin.defineElement({ name: "Resizable Panel", resizable: true, // 그리드에서 크기 조절 가능 // ... });

반응형 디자인 필수: resizable 옵션을 사용하려면 플러그인 내부 요소가 반응형으로 디자인되어야 합니다. 고정 크기(px) 대신 상대 단위(%, flex, fr 등)를 사용하고, 루트 요소에 width: 100%; height: 100%를 적용하세요. 그렇지 않으면 리사이즈해도 내부 콘텐츠가 컨테이너 크기에 맞게 조절되지 않습니다.

preserveAxis

설정이 변경될 때 유지할 크기 축을 지정합니다. resizable: true일 때만 적용됩니다.

설명
both가로와 세로 모두 유지 (기본값)
width가로 크기 유지, 세로는 콘텐츠에 맞춤
height세로 크기 유지, 가로는 콘텐츠에 맞춤
none둘 다 콘텐츠에 맞춤 (리사이즈 효과 리셋)
dmn.plugin.defineElement({ name: "KPS Panel", resizable: true, preserveAxis: "width", // 설정 변경 시 가로 크기 유지 // ... });

사용 예시

KPS 패널처럼 그래프 표시 옵션이 있는 경우:

  • preserveAxis: "width"로 설정하면 그래프 표시를 켜고 꺼도 가로 크기는 유지됩니다
  • 세로 크기는 그래프 유무에 따라 자동으로 조절됩니다
dmn.plugin.defineElement({ name: "KPS Panel", resizable: true, preserveAxis: "width", resizeAnchor: "bottom-left", // 하단 고정 (그래프가 위에서 펼쳐짐) settings: { showGraph: { type: "boolean", default: true, label: "그래프 표시" }, }, template: (state, settings, { html }) => html` <div style="width: 100%; height: 100%;"> <div class="kps-value">${state.kps ?? 0}</div> ${settings.showGraph ? html`<div class="graph">...</div>` : ""} </div> `, });

resizable 요소의 템플릿에서 루트 요소에 width: 100%; height: 100%를 적용하면 사용자가 조절한 크기에 맞게 내부 콘텐츠가 채워집니다.

컨텍스트 메뉴 (contextMenu)

기본 설정

contextMenu: { create: "패널 생성", // 그리드 빈 공간 우클릭 시 delete: "패널 삭제", // 패널 우클릭 시 },

커스텀 메뉴 항목

contextMenu: { create: "패널 생성", delete: "패널 삭제", items: [ { label: "통계 초기화", onClick: ({ actions }) => actions.reset(), }, { label: "데이터 내보내기", onClick: async ({ element, actions }) => { await actions.exportData(); }, // 조건부 표시 visible: ({ element }) => element.state.hasData, // 조건부 비활성화 disabled: ({ element }) => element.state.isLoading, // 위치 (top 또는 bottom) position: "bottom", }, ], }, // onMount에서 actions 등록 onMount: ({ expose, setState }) => { expose({ reset: () => setState({ count: 0 }), exportData: async () => { /* 내보내기 로직 */ }, }); },

템플릿 (template)

템플릿 함수는 statesettings를 받아 UI를 반환합니다. 자세한 문법은 템플릿 문법 문서를 참고하세요.

template: (state, settings, { html, t, locale }) => html` <div style="color: ${settings.textColor};"> <strong>${state.value}</strong> ${settings.showDetails ? html` <div class="details">상세 정보</div> ` : ""} </div> `,

템플릿 헬퍼

헬퍼설명
htmlhtm 태그 함수 (React Element 생성)
t(key)다국어 번역 함수
locale현재 언어 코드

미리보기 상태 (previewState)

메인 윈도우에서 보여줄 미리보기용 초기 상태입니다. 실제 로직은 오버레이에서만 실행되므로, 메인에서는 이 상태가 표시됩니다.

previewState: { kps: 12, history: [5, 8, 12, 10, 15], },

마운트 로직 (onMount)

onMount는 오버레이에서만 실행되며, 실제 동작 로직을 구현합니다.

Context 객체

onMount: (context) => { const { setState, // 상태 업데이트 getSettings, // 현재 설정 조회 setAnchor, // 리사이즈 앵커 설정 getAnchor, // 현재 앵커 조회 onHook, // 이벤트 훅 등록 expose, // 컨텍스트 메뉴용 함수 노출 locale, // 현재 언어 코드 t, // 번역 함수 onLocaleChange, // 언어 변경 구독 onSettingsChange, // 설정 변경 구독 } = context; // 클린업 함수 반환 return () => { /* 정리 작업 */ }; },

이벤트 훅 (onHook)

onMount: ({ onHook, setState }) => { // 매핑된 키 이벤트 onHook("key", ({ key, state, mode }) => { if (state === "DOWN") { console.log(`${key} 눌림 (${mode})`); } }); // 모든 원시 입력 이벤트 (키보드, 마우스) onHook("rawKey", ({ device, label, state }) => { console.log(`[${device}] ${label} ${state}`); }); },

설정 변경 감지 (onSettingsChange)

설정 변경에 즉시 반응해야 할 때 사용합니다.

대부분의 경우 getSettings()로 최신 설정을 조회하면 충분합니다. onSettingsChange는 외부 API 호출이나 리소스 재초기화가 필요한 경우에만 사용하세요.

onMount: ({ setState, getSettings, onSettingsChange }) => { const fetchData = async (nickname) => { const response = await fetch(`/api/user/${nickname}`); const data = await response.json(); setState({ data }); }; // 초기 로드 fetchData(getSettings().nickname); // 닉네임 변경 시 재요청 onSettingsChange((newSettings, oldSettings) => { if (newSettings.nickname !== oldSettings.nickname) { fetchData(newSettings.nickname); } }); },

다국어 지원 (messages)

dmn.plugin.defineElement({ name: "Localized Panel", messages: { ko: { "menu.create": "패널 생성", "menu.delete": "패널 삭제", "label.count": "카운트", }, en: { "menu.create": "Create Panel", "menu.delete": "Delete Panel", "label.count": "Count", }, }, contextMenu: { create: "menu.create", // 메시지 키 사용 delete: "menu.delete", }, settings: { count: { type: "number", default: 0, label: "label.count", // 메시지 키 사용 }, }, template: (state, settings, { html, t, locale }) => html` <div data-locale="${locale}">${t("label.count")}: ${state.value ?? 0}</div> `, });

실전 예제: KPS 패널

// @id kps-panel dmn.plugin.defineElement({ name: "KPS Panel", maxInstances: 1, contextMenu: { create: "KPS 패널 생성", delete: "KPS 패널 삭제", items: [ { label: "통계 초기화", onClick: ({ actions }) => actions.reset(), }, ], }, settings: { showGraph: { type: "boolean", default: true, label: "그래프 표시" }, textColor: { type: "color", default: "#FFFFFF", label: "텍스트 색상" }, graphColor: { type: "color", default: "#86EFAC", label: "그래프 색상" }, }, previewState: { kps: 12, max: 20, history: [5, 8, 12, 15, 10, 12], }, template: (state, settings, { html }) => html` <div style=" background: rgba(0, 0, 0, 0.8); padding: 16px; border-radius: 8px; color: ${settings.textColor}; min-width: 120px; " > <div style="font-size: 32px; font-weight: bold;"> ${state.kps ?? 0} <span style="font-size: 14px; opacity: 0.7;">KPS</span> </div> ${settings.showGraph ? html` <div style=" display: flex; gap: 2px; height: 40px; align-items: flex-end; margin-top: 8px; " > ${(state.history ?? []).map((v) => { const height = state.max ? (v / state.max) * 100 : 0; return html` <div style=" flex: 1; height: ${height}%; background: ${settings.graphColor}; border-radius: 2px 2px 0 0; opacity: 0.7; " ></div> `; })} </div> ` : ""} </div> `, onMount: ({ setState, expose, onHook }) => { const timestamps = []; let max = 0; const historySize = 20; const history = []; onHook("key", ({ state }) => { if (state === "DOWN") { timestamps.push(Date.now()); } }); const interval = setInterval(() => { const now = Date.now(); // 1초 이내 타임스탬프만 유지 while (timestamps.length && timestamps[0] < now - 1000) { timestamps.shift(); } const kps = timestamps.length; max = Math.max(max, kps); history.push(kps); if (history.length > historySize) history.shift(); setState({ kps, max, history: [...history] }); }, 50); expose({ reset: () => { timestamps.length = 0; history.length = 0; max = 0; setState({ kps: 0, max: 0, history: [] }); }, }); return () => clearInterval(interval); }, });