|
| 1 | +````mdx |
| 2 | +--- |
| 3 | +title: 플러그인 (plugin) |
| 4 | +description: dmn.plugin 네임스페이스 API 레퍼런스 |
| 5 | +--- |
| 6 | + |
| 7 | +# 플러그인 API (dmn.plugin) |
| 8 | + |
| 9 | +플러그인 생명주기, 스토리지, 선언형 UI 정의를 위한 API입니다. |
| 10 | + |
| 11 | +## 메서드 |
| 12 | + |
| 13 | +### registerCleanup |
| 14 | + |
| 15 | +플러그인 언로드 시 호출될 클린업 함수를 등록합니다. |
| 16 | + |
| 17 | +```typescript |
| 18 | +dmn.plugin.registerCleanup(cleanup: () => void): void |
| 19 | +``` |
| 20 | +```` |
| 21 | + |
| 22 | +**예시:** |
| 23 | + |
| 24 | +```javascript |
| 25 | +const timer = setInterval(() => console.log("tick"), 1000); |
| 26 | + |
| 27 | +dmn.plugin.registerCleanup(() => { |
| 28 | + clearInterval(timer); |
| 29 | + console.log("플러그인 정리 완료"); |
| 30 | +}); |
| 31 | +``` |
| 32 | + |
| 33 | +### storage |
| 34 | + |
| 35 | +플러그인별 격리된 스토리지입니다. 자세한 내용은 [스토리지](/plugin/storage) 문서를 참고하세요. |
| 36 | + |
| 37 | +```typescript |
| 38 | +dmn.plugin.storage.get(key: string): Promise<any> |
| 39 | +dmn.plugin.storage.set(key: string, value: any): Promise<void> |
| 40 | +dmn.plugin.storage.remove(key: string): Promise<void> |
| 41 | +dmn.plugin.storage.clear(): Promise<void> |
| 42 | +dmn.plugin.storage.keys(): Promise<string[]> |
| 43 | +``` |
| 44 | + |
| 45 | +### defineElement |
| 46 | + |
| 47 | +선언형 UI를 정의합니다. 자세한 내용은 [선언형 API](/plugin/declarative-api) 문서를 참고하세요. |
| 48 | + |
| 49 | +```typescript |
| 50 | +dmn.plugin.defineElement(definition: PluginDefinition): void |
| 51 | +``` |
| 52 | + |
| 53 | +## PluginDefinition |
| 54 | + |
| 55 | +`defineElement`에 전달하는 설정 객체입니다. |
| 56 | + |
| 57 | +```typescript |
| 58 | +interface PluginDefinition { |
| 59 | + /** 플러그인 이름 (컨텍스트 메뉴에 표시) */ |
| 60 | + name: string; |
| 61 | + |
| 62 | + /** 최대 인스턴스 개수 (0 = 무제한, 기본값) */ |
| 63 | + maxInstances?: number; |
| 64 | + |
| 65 | + /** |
| 66 | + * 리사이즈 앵커 (크기 변경 시 기준점) |
| 67 | + * @default "top-left" |
| 68 | + */ |
| 69 | + resizeAnchor?: ElementResizeAnchor; |
| 70 | + |
| 71 | + /** 컨텍스트 메뉴 설정 */ |
| 72 | + contextMenu?: { |
| 73 | + create?: string; // 생성 메뉴 라벨 |
| 74 | + delete?: string; // 삭제 메뉴 라벨 |
| 75 | + items?: PluginDefinitionContextMenuItem[]; |
| 76 | + }; |
| 77 | + |
| 78 | + /** 설정 스키마 */ |
| 79 | + settings?: Record<string, PluginSettingSchema>; |
| 80 | + |
| 81 | + /** 다국어 메시지 */ |
| 82 | + messages?: Record<string, Record<string, string>>; |
| 83 | + |
| 84 | + /** 미리보기 상태 (메인 윈도우용) */ |
| 85 | + previewState?: Record<string, any>; |
| 86 | + |
| 87 | + /** 템플릿 함수 */ |
| 88 | + template: ( |
| 89 | + state: Record<string, any>, |
| 90 | + settings: Record<string, any>, |
| 91 | + helpers: DisplayElementTemplateHelpers |
| 92 | + ) => DisplayElementTemplateResult | string; |
| 93 | + |
| 94 | + /** 마운트 로직 (오버레이에서만 실행) */ |
| 95 | + onMount?: (context: PluginDefinitionHookContext) => void | (() => void); |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +## ElementResizeAnchor |
| 100 | + |
| 101 | +요소 크기 변경 시 기준점을 지정하는 타입입니다. |
| 102 | + |
| 103 | +```typescript |
| 104 | +type ElementResizeAnchor = |
| 105 | + | "top-left" // 좌상단 (기본값) |
| 106 | + | "top-center" // 상단 중앙 |
| 107 | + | "top-right" // 우상단 |
| 108 | + | "center-left" // 좌측 중앙 |
| 109 | + | "center" // 정중앙 |
| 110 | + | "center-right" // 우측 중앙 |
| 111 | + | "bottom-left" // 좌하단 |
| 112 | + | "bottom-center" // 하단 중앙 |
| 113 | + | "bottom-right"; // 우하단 |
| 114 | +``` |
| 115 | + |
| 116 | +### 앵커 동작 |
| 117 | + |
| 118 | +크기가 변경될 때 지정한 앵커 위치가 고정되고, 다른 방향으로 확장/축소됩니다. |
| 119 | + |
| 120 | +| 앵커 | 동작 | |
| 121 | +| -------------- | ---------------------------- | |
| 122 | +| `top-left` | 우측, 하단으로 확장/축소 | |
| 123 | +| `center` | 모든 방향으로 균등 확장/축소 | |
| 124 | +| `bottom-right` | 좌측, 상단으로 확장/축소 | |
| 125 | + |
| 126 | +## PluginDefinitionHookContext |
| 127 | + |
| 128 | +`onMount` 콜백에 전달되는 컨텍스트 객체입니다. |
| 129 | + |
| 130 | +```typescript |
| 131 | +interface PluginDefinitionHookContext { |
| 132 | + /** 상태 업데이트 */ |
| 133 | + setState: (updates: Record<string, any>) => void; |
| 134 | + |
| 135 | + /** 현재 설정 조회 */ |
| 136 | + getSettings: () => Record<string, any>; |
| 137 | + |
| 138 | + /** 리사이즈 앵커 설정 */ |
| 139 | + setAnchor: (anchor: ElementResizeAnchor) => void; |
| 140 | + |
| 141 | + /** 현재 리사이즈 앵커 조회 */ |
| 142 | + getAnchor: () => ElementResizeAnchor; |
| 143 | + |
| 144 | + /** |
| 145 | + * 이벤트 훅 등록 |
| 146 | + * - "key": 매핑된 키 이벤트 |
| 147 | + * - "rawKey": 모든 원시 입력 이벤트 |
| 148 | + */ |
| 149 | + onHook: (event: "key" | "rawKey", callback: (...args: any[]) => void) => void; |
| 150 | + |
| 151 | + /** 컨텍스트 메뉴용 함수 노출 */ |
| 152 | + expose: (actions: Record<string, (...args: any[]) => any>) => void; |
| 153 | + |
| 154 | + /** 현재 언어 코드 */ |
| 155 | + locale: string; |
| 156 | + |
| 157 | + /** 번역 함수 */ |
| 158 | + t: PluginTranslateFn; |
| 159 | + |
| 160 | + /** 언어 변경 구독 */ |
| 161 | + onLocaleChange: (listener: (locale: string) => void) => Unsubscribe; |
| 162 | + |
| 163 | + /** 설정 변경 구독 */ |
| 164 | + onSettingsChange: ( |
| 165 | + listener: ( |
| 166 | + newSettings: Record<string, any>, |
| 167 | + oldSettings: Record<string, any> |
| 168 | + ) => void |
| 169 | + ) => void; |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +### setAnchor / getAnchor |
| 174 | + |
| 175 | +인스턴스별로 리사이즈 앵커를 동적으로 변경하거나 조회합니다. |
| 176 | + |
| 177 | +```javascript |
| 178 | +onMount: ({ setAnchor, getAnchor, getSettings }) => { |
| 179 | + // 현재 앵커 확인 |
| 180 | + console.log("현재 앵커:", getAnchor()); // "top-left" (기본값) |
| 181 | + |
| 182 | + // 앵커를 중앙으로 변경 |
| 183 | + setAnchor("center"); |
| 184 | + |
| 185 | + // 설정에 따라 앵커 변경 |
| 186 | + const settings = getSettings(); |
| 187 | + if (settings.expandFromCenter) { |
| 188 | + setAnchor("center"); |
| 189 | + } |
| 190 | +}, |
| 191 | +``` |
| 192 | + |
| 193 | +**앵커 우선순위:** |
| 194 | + |
| 195 | +1. `setAnchor()`로 설정한 인스턴스별 앵커 |
| 196 | +2. `PluginDefinition.resizeAnchor` (Definition 기본값) |
| 197 | +3. `"top-left"` (시스템 기본값) |
| 198 | + |
| 199 | +## PluginSettingSchema |
| 200 | + |
| 201 | +설정 스키마 정의 타입입니다. |
| 202 | + |
| 203 | +```typescript |
| 204 | +interface PluginSettingSchema { |
| 205 | + type: "boolean" | "color" | "number" | "string" | "select"; |
| 206 | + default: any; |
| 207 | + label: string; |
| 208 | + min?: number; // number 타입용 |
| 209 | + max?: number; // number 타입용 |
| 210 | + step?: number; // number 타입용 |
| 211 | + options?: { label: string; value: any }[]; // select 타입용 |
| 212 | + placeholder?: string; // string/number 타입용 |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | +## 예시 |
| 217 | + |
| 218 | +### 기본 플러그인 |
| 219 | + |
| 220 | +```javascript |
| 221 | +// @id simple-counter |
| 222 | + |
| 223 | +dmn.plugin.defineElement({ |
| 224 | + name: "Simple Counter", |
| 225 | + maxInstances: 3, |
| 226 | + resizeAnchor: "top-left", |
| 227 | + |
| 228 | + settings: { |
| 229 | + textColor: { |
| 230 | + type: "color", |
| 231 | + default: "#FFFFFF", |
| 232 | + label: "텍스트 색상", |
| 233 | + }, |
| 234 | + }, |
| 235 | + |
| 236 | + previewState: { |
| 237 | + count: 42, |
| 238 | + }, |
| 239 | + |
| 240 | + template: (state, settings, { html }) => html` |
| 241 | + <div style="color: ${settings.textColor}; padding: 16px;"> |
| 242 | + Count: ${state.count ?? 0} |
| 243 | + </div> |
| 244 | + `, |
| 245 | +
|
| 246 | + onMount: ({ setState, onHook }) => { |
| 247 | + let count = 0; |
| 248 | +
|
| 249 | + onHook("key", ({ state }) => { |
| 250 | + if (state === "DOWN") { |
| 251 | + count++; |
| 252 | + setState({ count }); |
| 253 | + } |
| 254 | + }); |
| 255 | + }, |
| 256 | +}); |
| 257 | +``` |
| 258 | +
|
| 259 | +### 동적 앵커 변경 |
| 260 | +
|
| 261 | +```javascript |
| 262 | +// @id dynamic-anchor-panel |
| 263 | +
|
| 264 | +dmn.plugin.defineElement({ |
| 265 | + name: "Dynamic Anchor Panel", |
| 266 | + resizeAnchor: "top-left", // 기본 앵커 |
| 267 | +
|
| 268 | + settings: { |
| 269 | + expandFromCenter: { |
| 270 | + type: "boolean", |
| 271 | + default: false, |
| 272 | + label: "중앙에서 확장", |
| 273 | + }, |
| 274 | + }, |
| 275 | +
|
| 276 | + template: (state, settings, { html }) => html` |
| 277 | + <div style="padding: 16px; background: rgba(0,0,0,0.8);"> |
| 278 | + 앵커: ${state.currentAnchor ?? "top-left"} |
| 279 | + </div> |
| 280 | + `, |
| 281 | +
|
| 282 | + onMount: ({ |
| 283 | + setState, |
| 284 | + getSettings, |
| 285 | + setAnchor, |
| 286 | + getAnchor, |
| 287 | + onSettingsChange, |
| 288 | + }) => { |
| 289 | + // 초기 앵커 설정 |
| 290 | + const settings = getSettings(); |
| 291 | + const anchor = settings.expandFromCenter ? "center" : "top-left"; |
| 292 | + setAnchor(anchor); |
| 293 | + setState({ currentAnchor: anchor }); |
| 294 | +
|
| 295 | + // 설정 변경 시 앵커 업데이트 |
| 296 | + onSettingsChange((newSettings, oldSettings) => { |
| 297 | + if (newSettings.expandFromCenter !== oldSettings.expandFromCenter) { |
| 298 | + const newAnchor = newSettings.expandFromCenter ? "center" : "top-left"; |
| 299 | + setAnchor(newAnchor); |
| 300 | + setState({ currentAnchor: newAnchor }); |
| 301 | + } |
| 302 | + }); |
| 303 | + }, |
| 304 | +}); |
| 305 | +``` |
| 306 | +
|
| 307 | +``` |
| 308 | +
|
| 309 | +``` |
0 commit comments