create-mendix-widget-gleam 4.0.1 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ my-widget/
28
28
  components/
29
29
  hello_world.gleam # Hello World 공유 컴포넌트
30
30
  package.json # npm 의존성 (React, 외부 라이브러리 등)
31
- gleam.toml # Gleam 프로젝트 설정 (glendix >= 4.0.3 + mendraw >= 1.1.11 의존성 포함)
31
+ gleam.toml # Gleam 프로젝트 설정 (glendix >= 4.0.4 + mendraw >= 1.2.1 의존성 포함)
32
32
  CLAUDE.md # AI 어시스턴트용 프로젝트 컨텍스트
33
33
  ```
34
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mendix-widget-gleam",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
4
4
  "description": "Scaffold a Mendix Pluggable Widget powered by Gleam",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -93,7 +93,7 @@ src/*.gleam → gleam build → build/dev/javascript/**/*.mjs → Bridge JS (aut
93
93
  For detailed glendix API and Gleam syntax, see:
94
94
 
95
95
  - docs/glendix_guide.md — Complete React/Mendix bindings guide (elements, Hooks, events, Mendix types, practical patterns, troubleshooting)
96
- - docs/mendraw_guide.md — mendraw usage guide (Marketplace download, classic widget support)
96
+ - docs/mendraw_guide.md — mendraw usage guide (widget binding generation, Marketplace download, Classic widgets, Synthetic Data, Mendix type API)
97
97
  - docs/gleam_language_tour.md — Gleam syntax reference (types, pattern matching, FFI, use keyword, etc.)
98
98
 
99
99
  ## Mendix Documentation Sources
@@ -125,7 +125,7 @@ src/
125
125
  package.json # npm dependencies (React, external libraries, etc.)
126
126
  \`\`\`
127
127
 
128
- React bindings come from [redraw](https://hexdocs.pm/redraw/)/[redraw_dom](https://hexdocs.pm/redraw_dom/), while Mendix API and JS Interop bindings are provided by [mendraw](https://hexdocs.pm/mendraw/).
128
+ React bindings come from [redraw](https://hexdocs.pm/redraw/)/[redraw_dom](https://hexdocs.pm/redraw_dom/), Mendix API bindings from [mendraw](https://hexdocs.pm/mendraw/), and JS Interop from [glendix](https://hexdocs.pm/glendix/).
129
129
 
130
130
  ## Using External React Components
131
131
 
@@ -293,7 +293,7 @@ src/
293
293
  package.json # npm 의존성 (React, 외부 라이브러리 등)
294
294
  \`\`\`
295
295
 
296
- React 바인딩은 [redraw](https://hexdocs.pm/redraw/)/[redraw_dom](https://hexdocs.pm/redraw_dom/)이, Mendix API JS Interop 바인딩은 [mendraw](https://hexdocs.pm/mendraw/)가 제공합니다.
296
+ React 바인딩은 [redraw](https://hexdocs.pm/redraw/)/[redraw_dom](https://hexdocs.pm/redraw_dom/)이, Mendix API 바인딩은 [mendraw](https://hexdocs.pm/mendraw/)가, JS Interop [glendix](https://hexdocs.pm/glendix/)가 제공합니다.
297
297
 
298
298
  ## 외부 React 컴포넌트 사용
299
299
 
@@ -461,7 +461,7 @@ src/
461
461
  package.json # npm依存関係(React、外部ライブラリなど)
462
462
  \`\`\`
463
463
 
464
- Reactバインディングは[redraw](https://hexdocs.pm/redraw/)/[redraw_dom](https://hexdocs.pm/redraw_dom/)が、Mendix APIおよびJS Interopバインディングは[mendraw](https://hexdocs.pm/mendraw/)が提供する。
464
+ Reactバインディングは[redraw](https://hexdocs.pm/redraw/)/[redraw_dom](https://hexdocs.pm/redraw_dom/)が、Mendix APIバインディングは[mendraw](https://hexdocs.pm/mendraw/)が、JS Interopは[glendix](https://hexdocs.pm/glendix/)が提供する。
465
465
 
466
466
  ## 外部Reactコンポーネントの使用
467
467
 
@@ -1,4 +1,4 @@
1
- # glendix v4.0.3 — Agent Reference Guide
1
+ # glendix v4.0.4 — Agent Reference Guide
2
2
 
3
3
  > 이 문서는 AI 에이전트(LLM)가 glendix 코드를 작성할 때 참조하는 가이드입니다. 각 섹션은 독립적으로 읽을 수 있습니다.
4
4
 
@@ -46,8 +46,8 @@ glendix는 Gleam으로 Mendix Pluggable Widget을 작성하는 FFI 라이브러
46
46
 
47
47
  ```toml
48
48
  [dependencies]
49
- glendix = ">= 4.0.3 and < 5.0.0"
50
- mendraw = ">= 1.1.11 and < 2.0.0"
49
+ glendix = ">= 4.0.4 and < 5.0.0"
50
+ mendraw = ">= 1.2.1 and < 2.0.0"
51
51
  ```
52
52
 
53
53
  Peer dependency (위젯 프로젝트 `package.json`):
@@ -1,7 +1,7 @@
1
1
  # mendraw 사용 가이드
2
2
 
3
- Mendix 위젯 Gleam/[redraw](https://hexdocs.pm/redraw/) 바인딩 라이브러리.
4
- Classic(Dojo) 위젯 지원과 Marketplace 검색/다운로드 기능을 제공한다.
3
+ Mendix 위젯 `.mpk` 파일에서 Gleam/[redraw](https://hexdocs.pm/redraw/) 바인딩을 자동 생성하는 라이브러리.
4
+ Pluggable(React) 위젯과 Classic(Dojo) 위젯을 모두 지원한다.
5
5
 
6
6
  ---
7
7
 
@@ -9,18 +9,36 @@ Classic(Dojo) 위젯 지원과 Marketplace 검색/다운로드 기능을 제공
9
9
 
10
10
  - [사전 준비](#사전-준비)
11
11
  - [설치](#설치)
12
+ - [빠른 시작](#빠른-시작)
12
13
  - [Marketplace에서 위젯 다운로드](#marketplace에서-위젯-다운로드)
13
- - [Classic (Dojo) 위젯](#classic-dojo-위젯)
14
- - [Classic 위젯 직접 렌더링](#classic-위젯-직접-렌더링)
14
+ - [생성된 바인딩 사용하기](#생성된-바인딩-사용하기)
15
+ - [Pluggable 위젯](#pluggable-위젯)
16
+ - [Classic (Dojo) 위젯](#classic-dojo-위젯)
17
+ - [저수준 API로 직접 조립하기](#저수준-api로-직접-조립하기)
18
+ - [위젯 컴포넌트 조회](#위젯-컴포넌트-조회)
19
+ - [Prop 래핑](#prop-래핑)
20
+ - [컴포넌트 렌더링](#컴포넌트-렌더링)
15
21
  - [JsProps 다루기](#jsprops-다루기)
16
22
  - [Prop 접근자](#prop-접근자)
17
23
  - [ValueStatus](#valuestatus)
18
24
  - [Option 변환](#option-변환)
25
+ - [생성된 코드 구조](#생성된-코드-구조)
26
+ - [Pluggable 위젯 바인딩 예시](#pluggable-위젯-바인딩-예시)
27
+ - [Classic 위젯 바인딩 예시](#classic-위젯-바인딩-예시)
28
+ - [파일명 변환 규칙](#파일명-변환-규칙)
29
+ - [생성된 바인딩 커스터마이징](#생성된-바인딩-커스터마이징)
19
30
  - [API 레퍼런스](#api-레퍼런스)
20
31
  - [mendraw/mendix](#mendrawmendix)
32
+ - [mendraw/widget](#mendrawwidget)
21
33
  - [mendraw/interop](#mendrawinterop)
22
34
  - [mendraw/classic](#mendrawclassic)
23
35
  - [mendraw/marketplace](#mendrawmarketplace)
36
+ - [외부 API 데이터를 Mendix 위젯에 전달 (Synthetic Data)](#외부-api-데이터를-mendix-위젯에-전달-synthetic-data)
37
+ - [개요](#synthetic-개요)
38
+ - [기본 사용법](#synthetic-기본-사용법)
39
+ - [차트 위젯에 사용하기](#차트-위젯에-사용하기)
40
+ - [DynamicDataGrid에 사용하기](#dynamicdatagrid에-사용하기)
41
+ - [API 레퍼런스 (synthetic)](#api-레퍼런스-synthetic)
24
42
  - [glendix 프로젝트에서 사용하기](#glendix-프로젝트에서-사용하기)
25
43
  - [문제 해결](#문제-해결)
26
44
 
@@ -45,7 +63,7 @@ gleam add mendraw@1
45
63
 
46
64
  ```toml
47
65
  [dependencies]
48
- mendraw = ">= 1.1.11 and < 2.0.0"
66
+ mendraw = ">= 1.0.0 and < 2.0.0"
49
67
  ```
50
68
 
51
69
  mendraw는 `gleam_stdlib`, `gleam_javascript`, `redraw`, `redraw_dom`을 함께 가져온다.
@@ -53,9 +71,61 @@ mendraw는 `gleam_stdlib`, `gleam_javascript`, `redraw`, `redraw_dom`을 함께
53
71
 
54
72
  ---
55
73
 
74
+ ## 빠른 시작
75
+
76
+ ### 1단계: 위젯 등록
77
+
78
+ **`gleam.toml`로 자동 다운로드**
79
+
80
+ ```toml
81
+ [tools.mendraw.widgets.Charts]
82
+ version = "3.0.0"
83
+ # s3_id = "com/..." ← 있으면 인증 없이 직접 다운로드
84
+ ```
85
+
86
+ `gleam run -m mendraw/install` 실행 시 `build/widgets/`에 캐시하고 바인딩을 자동 생성한다.
87
+ Marketplace TUI(`gleam run -m mendraw/marketplace`)에서 다운로드하면 gleam.toml에 자동 추가된다.
88
+
89
+ ### 2단계: 바인딩 생성
90
+
91
+ ```sh
92
+ gleam run -m mendraw/install
93
+ ```
94
+
95
+ 실행 결과:
96
+
97
+ - TOML에 등록된 위젯을 `build/widgets/`에 다운로드/캐시
98
+ - `src/widgets/`에 위젯별 `.gleam` 바인딩 파일이 생성된다
99
+ - 빌드 경로에 `widget_ffi.mjs`(컴포넌트 레지스트리)가 생성된다
100
+ - Classic 위젯이 있으면 `classic_ffi.mjs`(런타임)도 생성된다
101
+
102
+ ```
103
+ src/widgets/
104
+ ├── switch.gleam ← Pluggable 위젯
105
+ ├── area_chart.gleam ← Charts.mpk에서 추출
106
+ ├── bar_chart.gleam
107
+ └── camera_widget.gleam ← Classic 위젯
108
+ ```
109
+
110
+ ### 3단계: 바인딩 사용
111
+
112
+ ```gleam
113
+ import widgets/switch
114
+ import mendraw/mendix.{type JsProps}
115
+ import redraw.{type Element}
116
+
117
+ pub fn my_view(props: JsProps) -> Element {
118
+ switch.render(props)
119
+ }
120
+ ```
121
+
122
+ 이것으로 끝이다. 아래에서 각 단계를 자세히 설명한다.
123
+
124
+ ---
125
+
56
126
  ## Marketplace에서 위젯 다운로드
57
127
 
58
- Mendix Marketplace에서 위젯을 검색하고 다운로드할 수 있는 TUI를 제공한다.
128
+ `.mpk` 파일을 직접 구하는 대신, Mendix Marketplace에서 위젯을 검색하고 다운로드할 수 있는 TUI를 제공한다.
59
129
 
60
130
  ### 사전 설정
61
131
 
@@ -112,7 +182,8 @@ gleam run -m mendraw/marketplace
112
182
 
113
183
  ### 다운로드 후
114
184
 
115
- 다운로드한 위젯은 `gleam.toml`의 `[tools.mendraw.widgets.*]`에 자동 추가된다.
185
+ 다운로드한 위젯은 `build/widgets/`에 캐시되고 `gleam.toml`의 `[tools.mendraw.widgets.*]`에 자동 추가된다.
186
+ 다운로드가 1개 이상 완료되면 자동으로 `cmd.generate_widget_bindings()`가 실행되어 바인딩이 생성된다.
116
187
 
117
188
  > **참고**: 첫 다운로드 시 chrobot_extra 사이드카를 통한 Mendix 로그인이 필요할 수 있다.
118
189
  > 로그인 세션은 `.marketplace-cache/session.json`에 캐시된다.
@@ -120,7 +191,124 @@ gleam run -m mendraw/marketplace
120
191
 
121
192
  ---
122
193
 
123
- ## Classic (Dojo) 위젯
194
+ ## 생성된 바인딩 사용하기
195
+
196
+ `gleam run -m mendraw/install`이 생성하는 `.gleam` 파일에는 `render` 함수가 포함된다.
197
+ 이 함수는 Mendix가 전달하는 `JsProps`를 받아 `redraw` `Element`를 반환한다.
198
+
199
+ ### Pluggable 위젯
200
+
201
+ Pluggable 위젯(React 기반)의 생성된 바인딩:
202
+
203
+ ```gleam
204
+ import widgets/switch
205
+ import mendraw/mendix.{type JsProps}
206
+ import redraw.{type Element}
207
+
208
+ /// Mendix가 위젯에 전달하는 props를 그대로 넘긴다
209
+ pub fn view(props: JsProps) -> Element {
210
+ switch.render(props)
211
+ }
212
+ ```
213
+
214
+ 생성된 `render` 함수는 내부적으로:
215
+ 1. `mendix.get_prop_required`/`mendix.get_prop`으로 props에서 속성을 추출
216
+ 2. `widget.component`로 원본 React 컴포넌트를 가져옴
217
+ 3. `interop.component_el`로 redraw Element를 생성
218
+
219
+ ### Classic (Dojo) 위젯
220
+
221
+ Classic 위젯(Dojo 기반)의 생성된 바인딩:
222
+
223
+ ```gleam
224
+ import widgets/camera_widget
225
+ import mendraw/mendix.{type JsProps}
226
+ import redraw.{type Element}
227
+
228
+ pub fn view(props: JsProps) -> Element {
229
+ camera_widget.render(props)
230
+ }
231
+ ```
232
+
233
+ Classic 위젯은 내부적으로 DOM 컨테이너를 생성하고, `useEffect`로 위젯의 마운트/언마운트를 관리한다.
234
+
235
+ ---
236
+
237
+ ## 저수준 API로 직접 조립하기
238
+
239
+ 생성된 바인딩 대신, `mendraw/widget`과 `mendraw/interop` 모듈을 직접 사용하여
240
+ 위젯 렌더링을 세밀하게 제어할 수 있다.
241
+
242
+ ### 위젯 컴포넌트 조회
243
+
244
+ ```gleam
245
+ import mendraw/widget
246
+
247
+ // 위젯 이름으로 React 컴포넌트를 가져온다
248
+ let comp = widget.component("Switch")
249
+ ```
250
+
251
+ 위젯 이름은 `.mpk` 파일의 `widget.xml`에 정의된 `<name>` 값이다.
252
+
253
+ ### Prop 래핑
254
+
255
+ Mendix 위젯은 일반 값이 아닌 래핑된 값 객체를 기대한다.
256
+ mendraw는 세 가지 prop 래퍼를 제공한다:
257
+
258
+ ```gleam
259
+ import mendraw/widget
260
+
261
+ // 1. 읽기 전용 prop (DynamicValue)
262
+ // expression, textTemplate 등 읽기 전용 속성에 사용
263
+ widget.prop("caption", "제목 텍스트")
264
+
265
+ // 2. 편집 가능한 prop (EditableValue)
266
+ // 사용자 입력이 필요한 속성에 사용
267
+ widget.editable_prop("textAttr", current_value, "표시값", fn(new_val) {
268
+ // 값이 변경되었을 때의 처리
269
+ Nil
270
+ })
271
+
272
+ // 3. 액션 prop (ActionValue)
273
+ // onClick, onLeave 등 이벤트 핸들러에 사용
274
+ widget.action_prop("onClick", fn() {
275
+ // 클릭 시 처리
276
+ Nil
277
+ })
278
+ ```
279
+
280
+ 각 래퍼가 생성하는 Mendix 값 객체:
281
+
282
+ | 래퍼 | Mendix 타입 | 구조 |
283
+ |------|------------|------|
284
+ | `prop` | `DynamicValue` | `{ status: "available", value }` |
285
+ | `editable_prop` | `EditableValue` | `{ status: "available", value, displayValue, readOnly: false, setValue, ... }` |
286
+ | `action_prop` | `ActionValue` | `{ canExecute: true, isExecuting: false, execute }` |
287
+
288
+ ### 컴포넌트 렌더링
289
+
290
+ ```gleam
291
+ import mendraw/interop
292
+ import mendraw/widget
293
+ import redraw/dom/attribute
294
+
295
+ let comp = widget.component("Switch")
296
+
297
+ // 속성 + 자식 엘리먼트
298
+ interop.component_el(comp, [
299
+ widget.prop("caption", "제목"),
300
+ widget.editable_prop("textAttr", value, "표시값", set_value),
301
+ widget.action_prop("onClick", handler),
302
+ ], [])
303
+
304
+ // 속성 없이 자식만
305
+ interop.component_el_(comp, [child1, child2])
306
+
307
+ // 자식 없는 self-closing 컴포넌트
308
+ interop.void_component_el(comp, [
309
+ widget.prop("caption", "읽기 전용"),
310
+ ])
311
+ ```
124
312
 
125
313
  ### Classic 위젯 직접 렌더링
126
314
 
@@ -213,6 +401,118 @@ let js_value = mendix.from_option(gleam_option)
213
401
 
214
402
  ---
215
403
 
404
+ ## 생성된 코드 구조
405
+
406
+ ### Pluggable 위젯 바인딩 예시
407
+
408
+ Switch 위젯에서 생성되는 `src/widgets/switch.gleam`:
409
+
410
+ ```gleam
411
+ // @generated mendraw/install — 직접 수정 금지
412
+
413
+ import gleam/option.{None, Some}
414
+ import mendraw/interop
415
+ import mendraw/mendix.{type JsProps}
416
+ import mendraw/widget
417
+ import redraw.{type Element}
418
+ import redraw/dom/attribute
419
+
420
+ pub fn render(props: JsProps) -> Element {
421
+ // 필수 속성
422
+ let text_attr = mendix.get_prop_required(props, "textAttr")
423
+ // 선택 속성
424
+ let caption = mendix.get_prop(props, "caption")
425
+
426
+ let comp = widget.component("Switch")
427
+ interop.component_el(
428
+ comp,
429
+ [
430
+ attribute.attribute("textAttr", text_attr),
431
+ // 선택 속성은 있을 때만 전달
432
+ ..optional_attr("caption", caption)
433
+ ],
434
+ [],
435
+ )
436
+ }
437
+
438
+ fn optional_attr(key: String, value: option.Option(a)) -> List(attribute.Attribute) {
439
+ case value {
440
+ Some(v) -> [attribute.attribute(key, v)]
441
+ None -> []
442
+ }
443
+ }
444
+ ```
445
+
446
+ ### Classic 위젯 바인딩 예시
447
+
448
+ CameraWidget 위젯에서 생성되는 `src/widgets/camera_widget.gleam`:
449
+
450
+ ```gleam
451
+ // @generated mendraw/install — 직접 수정 금지
452
+
453
+ import gleam/option.{None, Some}
454
+ import mendraw/classic
455
+ import mendraw/mendix.{type JsProps}
456
+ import redraw.{type Element}
457
+
458
+ pub fn render(props: JsProps) -> Element {
459
+ let mf_to_execute = mendix.get_prop_required(props, "mfToExecute")
460
+ let prefer_rear_camera = mendix.get_prop_required(props, "preferRearCamera")
461
+
462
+ classic.render("CameraWidget.widget.CameraWidget", [
463
+ #("mfToExecute", classic.to_dynamic(mf_to_execute)),
464
+ #("preferRearCamera", classic.to_dynamic(prefer_rear_camera)),
465
+ ])
466
+ }
467
+ ```
468
+
469
+ ### 파일명 변환 규칙
470
+
471
+ 위젯 이름은 Gleam 모듈명 규칙에 맞게 snake_case로 변환된다:
472
+
473
+ | 위젯 이름 | 파일명 | 모듈 경로 |
474
+ |-----------|--------|----------|
475
+ | `Switch` | `switch.gleam` | `widgets/switch` |
476
+ | `AreaChart` | `area_chart.gleam` | `widgets/area_chart` |
477
+ | `BarChart` | `bar_chart.gleam` | `widgets/bar_chart` |
478
+ | `CameraWidget` | `camera_widget.gleam` | `widgets/camera_widget` |
479
+ | `Progress Bar` | `progress_bar.gleam` | `widgets/progress_bar` |
480
+
481
+ ---
482
+
483
+ ## 생성된 바인딩 커스터마이징
484
+
485
+ 생성된 `src/widgets/*.gleam` 파일은 한 번 생성된 후 **덮어쓰지 않는다**.
486
+ 따라서 생성된 파일을 직접 수정하여 커스터마이징할 수 있다:
487
+
488
+ ```gleam
489
+ // src/widgets/switch.gleam — 사용자가 수정한 버전
490
+ import mendraw/interop
491
+ import mendraw/mendix.{type JsProps}
492
+ import mendraw/widget
493
+ import redraw.{type Element}
494
+ import redraw/dom/attribute
495
+
496
+ pub fn render(props: JsProps) -> Element {
497
+ let text_attr = mendix.get_prop_required(props, "textAttr")
498
+ let comp = widget.component("Switch")
499
+ interop.component_el(
500
+ comp,
501
+ [
502
+ widget.editable_prop("textAttr", text_attr, "표시값", fn(v) { Nil }),
503
+ // 커스텀: 고정 캡션 추가
504
+ widget.prop("caption", "내 스위치"),
505
+ ],
506
+ [],
507
+ )
508
+ }
509
+ ```
510
+
511
+ > **주의**: 수정한 파일은 `gleam run -m mendraw/install`을 다시 실행해도 덮어쓰지 않는다.
512
+ > 바인딩을 재생성하려면 해당 파일을 삭제한 후 install을 다시 실행한다.
513
+
514
+ ---
515
+
216
516
  ## API 레퍼런스
217
517
 
218
518
  ### mendraw/mendix
@@ -241,6 +541,17 @@ Mendix Pluggable Widget API의 핵심 타입과 props 접근자.
241
541
  | `to_option` | `(a) -> Option(a)` | JS undefined/null → `None` |
242
542
  | `from_option` | `(Option(a)) -> a` | Gleam `Option` → JS 값 (`None` → undefined) |
243
543
 
544
+ ### mendraw/widget
545
+
546
+ Pluggable 위젯 컴포넌트 조회 및 prop 래핑.
547
+
548
+ | 함수 | 시그니처 | 설명 |
549
+ |------|----------|------|
550
+ | `component` | `(String) -> JsComponent` | 이름으로 위젯 컴포넌트 조회 |
551
+ | `prop` | `(String, a) -> Attribute` | `DynamicValue`로 래핑 (읽기 전용) |
552
+ | `editable_prop` | `(String, a, String, fn(a) -> Nil) -> Attribute` | `EditableValue`로 래핑 (편집 가능) |
553
+ | `action_prop` | `(String, fn() -> Nil) -> Attribute` | `ActionValue`로 래핑 (이벤트 핸들러) |
554
+
244
555
  ### mendraw/interop
245
556
 
246
557
  외부 JS React 컴포넌트를 redraw Element로 변환하는 브릿지.
@@ -274,8 +585,10 @@ Classic (Dojo) 위젯을 React 내부에서 렌더링.
274
585
  | 함수 | 시그니처 | 설명 |
275
586
  |------|----------|------|
276
587
  | `file_exists` | `(String) -> Bool` | 파일 존재 여부 |
277
- | `resolve_toml_widgets` | `() -> Nil` | gleam.toml [tools.mendraw.widgets.*] 다운로드 |
588
+ | `generate_widget_bindings` | `() -> Nil` | build/widgets/ 캐시에서 바인딩 생성 |
589
+ | `resolve_toml_widgets` | `() -> Nil` | gleam.toml [tools.mendraw.widgets.*] 다운로드/캐시 |
278
590
  | `write_widget_toml` | `(String, String, Option(Int), Option(String)) -> Nil` | gleam.toml에 위젯 항목 쓰기 |
591
+ | `download_to_cache` | `(String, String, String, Option(Int)) -> Bool` | URL에서 build/widgets/{name}/에 다운로드+추출 |
279
592
 
280
593
  ### mendraw/marketplace
281
594
 
@@ -289,6 +602,8 @@ Mendix Marketplace 위젯 검색·다운로드 TUI. `gleam run -m mendraw/market
289
602
  | 백그라운드 로딩 | 전체 위젯 목록을 백그라운드에서 점진적으로 로드 |
290
603
  | 버전 선택 | Content API + chrobot_extra 사이드카(XAS)로 버전별 다운로드 정보 조회 |
291
604
  | 자동 TOML 기록 | 다운로드 시 gleam.toml에 위젯 항목 자동 추가 |
605
+ | 캐시 다운로드 | build/widgets/에 캐시 (소스 컨트롤에 .mpk 불필요) |
606
+ | 자동 바인딩 | 다운로드 완료 후 `generate_widget_bindings()` 자동 호출 |
292
607
 
293
608
  #### 의존성
294
609
 
@@ -298,10 +613,211 @@ Mendix Marketplace 위젯 검색·다운로드 TUI. `gleam run -m mendraw/market
298
613
 
299
614
  ---
300
615
 
616
+ ## 외부 API 데이터를 Mendix 위젯에 전달 (Synthetic Data)
617
+
618
+ ### Synthetic 개요
619
+
620
+ Mendix 차트/그리드 위젯은 `ListValue`, `ObjectItem`, `ListAttributeValue` 등 Mendix 런타임에서만 생성되는 opaque 객체를 데이터 소스로 사용한다. `mendraw/synthetic` 모듈은 이 인터페이스를 모사하는 객체를 Gleam에서 직접 생성하여, 외부 API 데이터(CoinGecko, 날씨 API 등)를 Mendix Marketplace 위젯에 직접 전달할 수 있게 한다.
621
+
622
+ 지원하는 위젯 유형:
623
+ - **Charts** (Line, Area, Bar, Column, Pie, Time Series, Heat Map, Bubble, Custom)
624
+ - **Dynamic Data Grid** (3단계 cell/row/column 계층 포함)
625
+
626
+ ### Synthetic 기본 사용법
627
+
628
+ ```gleam
629
+ import mendraw/synthetic
630
+ import gleam/float
631
+
632
+ // 1. ObjectItem 생성 — 데이터 행 수만큼
633
+ let items = synthetic.object_items(3) // synth_0, synth_1, synth_2
634
+
635
+ // 2. ListValue 생성 — ObjectItem 목록을 감싸는 데이터 소스
636
+ let lv = synthetic.list_value(items)
637
+
638
+ // 3. ListAttributeValue 생성 — 각 아이템의 특정 필드값 + 표시 함수 + Mendix 타입
639
+ let prices = [100.0, 200.0, 150.0]
640
+ let price_attr = synthetic.list_attribute(items, prices, float.to_string, "Decimal")
641
+ // price_attr.get(synth_0) → EditableValue { value: BigNumber(100.0), displayValue: "100.0" }
642
+
643
+ // 4. TextTemplate 생성
644
+ let static_name = synthetic.text_template("My Series")
645
+ // chart에서 .value로 직접 접근하거나 .get(item)으로 접근 모두 지원
646
+
647
+ // 5. List-bound TextTemplate — 아이템별 다른 텍스트
648
+ let names = ["Bitcoin", "Ethereum", "Solana"]
649
+ let name_tmpl = synthetic.list_text_template(items, names)
650
+ // name_tmpl.get(synth_0) → { status: "available", value: "Bitcoin" }
651
+ ```
652
+
653
+ **타입 래핑 규칙:**
654
+
655
+ | Mendix 타입 | Gleam 값 | JS 래핑 |
656
+ |---|---|---|
657
+ | `"Decimal"`, `"Integer"`, `"Long"` | `Float` / `Int` | Big.js 호환 객체 (`.toNumber()`, `.toFixed()` 등) |
658
+ | `"DateTime"` | `Float` (밀리초 타임스탬프) | `new Date(timestamp)` |
659
+ | `"String"` | `String` | 그대로 전달 |
660
+
661
+ ### 차트 위젯에 사용하기
662
+
663
+ Mendix 차트 위젯의 `lines`/`series` prop은 시리즈 설정 객체의 JS 배열을 기대한다.
664
+ `chart_series_static()`으로 이 객체를 생성하고, `to_js_array()`로 Gleam List를 JS Array로 변환한다.
665
+
666
+ #### Line Chart / Time Series 예시
667
+
668
+ ```gleam
669
+ import mendraw/synthetic
670
+ import mendraw/interop
671
+ import mendraw/widget
672
+ import redraw/dom/attribute
673
+ import gleam/float
674
+
675
+ let items = synthetic.object_items(100)
676
+ let lv = synthetic.list_value(items)
677
+ let x_attr = synthetic.list_attribute(items, timestamps, float.to_string, "DateTime")
678
+ let y_attr = synthetic.list_attribute(items, prices, float.to_string, "Decimal")
679
+
680
+ // 시리즈 설정 객체 생성
681
+ let series = synthetic.chart_series_static(
682
+ lv, x_attr, y_attr,
683
+ synthetic.text_template("BTC Price"),
684
+ "none", // aggregation: "none" | "count" | "sum" | "avg" ...
685
+ "linear", // interpolation: "linear" | "spline"
686
+ "line", // lineStyle: "line" | "lines+markers"
687
+ "", // lineColor (빈 문자열 = 기본값)
688
+ "", // barColor
689
+ )
690
+
691
+ let comp = widget.component("Line chart")
692
+ interop.component_el(comp, [
693
+ attribute.attribute("lines", synthetic.to_js_array([series])),
694
+ attribute.attribute("showLegend", True),
695
+ attribute.attribute("gridLines", "horizontal"),
696
+ attribute.attribute("widthUnit", "percentage"),
697
+ attribute.attribute("width", 100),
698
+ attribute.attribute("heightUnit", "pixels"),
699
+ attribute.attribute("height", 400),
700
+ ], [])
701
+ ```
702
+
703
+ Time Series 위젯도 동일한 구조이며, `showRangeSlider` prop을 추가하면 범위 슬라이더가 표시된다.
704
+
705
+ #### Pie Chart 예시
706
+
707
+ Pie Chart는 `seriesDataSource`, `seriesName`, `seriesValueAttribute`를 직접 전달한다 (시리즈 배열 없음).
708
+
709
+ ```gleam
710
+ let items = synthetic.object_items(10)
711
+ let lv = synthetic.list_value(items)
712
+ let name_tmpl = synthetic.list_text_template(items, coin_names)
713
+ let value_attr = synthetic.list_attribute(items, market_caps, float.to_string, "Decimal")
714
+
715
+ let comp = widget.component("Pie chart")
716
+ interop.component_el(comp, [
717
+ attribute.attribute("seriesDataSource", lv),
718
+ attribute.attribute("seriesName", name_tmpl),
719
+ attribute.attribute("seriesValueAttribute", value_attr),
720
+ attribute.attribute("showLegend", True),
721
+ ], [])
722
+ ```
723
+
724
+ #### Column / Bar Chart 예시
725
+
726
+ Column/Bar 차트는 Line Chart와 동일한 시리즈 구조를 사용하되, prop 키가 `"series"`이다.
727
+
728
+ ```gleam
729
+ let series = synthetic.chart_series_static(
730
+ lv, x_attr, y_attr,
731
+ synthetic.text_template("Volume"),
732
+ "none", "", "", "", "#3b82f6", // barColor 지정
733
+ )
734
+
735
+ let comp = widget.component("Column chart")
736
+ interop.component_el(comp, [
737
+ attribute.attribute("series", synthetic.to_js_array([series])),
738
+ // ...
739
+ ], [])
740
+ ```
741
+
742
+ ### DynamicDataGrid에 사용하기
743
+
744
+ DynamicDataGrid는 3단계 계층 구조를 사용한다:
745
+ - **Row** — 행 (예: 각 코인)
746
+ - **Column** — 열 (예: Price, Volume, Market Cap)
747
+ - **Cell** — 행×열 교차점의 값 (N rows × M columns = N*M cells)
748
+
749
+ Cell은 `referenceRow`와 `referenceColumn` association으로 자신이 속한 행/열을 참조한다.
750
+
751
+ ```gleam
752
+ import mendraw/synthetic
753
+
754
+ let num_rows = 20 // 코인 수
755
+ let num_cols = 5 // 속성 수
756
+ let num_cells = num_rows * num_cols
757
+
758
+ let row_items = synthetic.object_items(num_rows)
759
+ let col_items = synthetic.object_items(num_cols)
760
+ let cell_items = synthetic.object_items(num_cells)
761
+
762
+ // 데이터 소스
763
+ let cell_source = synthetic.list_value(cell_items)
764
+ let row_source = synthetic.list_value(row_items)
765
+ let col_source = synthetic.list_value(col_items)
766
+
767
+ // Cell→Row, Cell→Column 참조 (각 cell이 어느 행/열에 속하는지)
768
+ let ref_row = synthetic.association(cell_items, cell_to_row_mapping)
769
+ let ref_col = synthetic.association(cell_items, cell_to_col_mapping)
770
+
771
+ // 표시 속성
772
+ let cell_attr = synthetic.list_attribute(cell_items, cell_values, fn(s) { s }, "String")
773
+ let row_attr = synthetic.list_attribute(row_items, row_names, fn(s) { s }, "String")
774
+ let col_attr = synthetic.list_attribute(col_items, column_headers, fn(s) { s }, "String")
775
+
776
+ let comp = widget.component("Dynamic data grid")
777
+ interop.component_el(comp, [
778
+ attribute.attribute("dataSourceCell", cell_source),
779
+ attribute.attribute("dataSourceRow", row_source),
780
+ attribute.attribute("dataSourceColumn", col_source),
781
+ attribute.attribute("referenceRow", ref_row),
782
+ attribute.attribute("referenceColumn", ref_col),
783
+ attribute.attribute("showCellAs", "attribute"),
784
+ attribute.attribute("cellAttribute", cell_attr),
785
+ attribute.attribute("showRowAs", "attribute"),
786
+ attribute.attribute("rowAttribute", row_attr),
787
+ attribute.attribute("showHeaderAs", "attribute"),
788
+ attribute.attribute("headerAttribute", col_attr),
789
+ attribute.attribute("renderAs", "table"),
790
+ ], [])
791
+ ```
792
+
793
+ ### API 레퍼런스 (synthetic)
794
+
795
+ #### 데이터 생성
796
+
797
+ | 함수 | 시그니처 | 설명 |
798
+ |------|----------|------|
799
+ | `object_item` | `(String) -> ObjectItem` | 지정 id로 ObjectItem 생성 |
800
+ | `object_items` | `(Int) -> List(ObjectItem)` | N개 ObjectItem 생성 (`synth_0`, `synth_1`, ...) |
801
+ | `list_value` | `(List(ObjectItem)) -> ListValue` | ObjectItem 목록을 ListValue로 래핑 |
802
+ | `list_attribute` | `(List(ObjectItem), List(a), fn(a) -> String, String) -> ListAttributeValue` | 아이템별 속성값 + 표시함수 + Mendix 타입 |
803
+ | `text_template` | `(String) -> a` | 정적 텍스트 템플릿 (`.value` + `.get()` 양쪽 지원) |
804
+ | `list_text_template` | `(List(ObjectItem), List(String)) -> ListExpressionValue` | 아이템별 텍스트 템플릿 |
805
+ | `list_expression` | `(List(ObjectItem), List(a)) -> ListExpressionValue` | 아이템별 표현식 값 |
806
+ | `association` | `(List(ObjectItem), List(ObjectItem)) -> a` | 소스→타겟 연관 관계 |
807
+
808
+ #### 차트 시리즈
809
+
810
+ | 함수 | 시그니처 | 설명 |
811
+ |------|----------|------|
812
+ | `chart_series_static` | `(ListValue, ListAttributeValue, ListAttributeValue, a, String, String, String, String, String) -> b` | 정적 차트 시리즈 설정 객체 생성 (dataSource, x, y, name, aggregation, interpolation, lineStyle, lineColor, barColor) |
813
+ | `to_js_array` | `(List(a)) -> b` | Gleam List → JS Array 변환 |
814
+
815
+ ---
816
+
301
817
  ## glendix 프로젝트에서 사용하기
302
818
 
303
819
  [glendix](https://github.com/) 프로젝트에서 mendraw를 의존성으로 추가하면,
304
- 위젯 TOML 해석을 mendraw에 위임할 수 있다:
820
+ MPK 바인딩 생성을 mendraw에 위임할 수 있다:
305
821
 
306
822
  ```gleam
307
823
  // glendix의 install.gleam
@@ -310,7 +826,8 @@ import mendraw/cmd as mendraw_cmd
310
826
  pub fn main() {
311
827
  cmd.exec(cmd.detect_install_command())
312
828
  cmd.generate_bindings()
313
- mendraw_cmd.resolve_toml_widgets()
829
+ // MPK 위젯 바인딩 생성을 mendraw에 위임
830
+ mendraw_cmd.generate_widget_bindings()
314
831
  }
315
832
  ```
316
833
 
@@ -318,11 +835,61 @@ pub fn main() {
318
835
 
319
836
  ## 문제 해결
320
837
 
838
+ ### `gleam run -m mendraw/install` 실행 시 아무것도 생성되지 않는다
839
+
840
+ - `gleam.toml`에 `[tools.mendraw.widgets.*]` 섹션이 있는지 확인
841
+ - `.mpk` 파일이 유효한 ZIP 형식인지 확인
842
+ - 콘솔 출력을 확인하여 파싱 오류가 없는지 점검
843
+
844
+ ### 이미 존재하는 바인딩 파일이 업데이트되지 않는다
845
+
846
+ mendraw는 `src/widgets/`에 이미 존재하는 `.gleam` 파일을 **덮어쓰지 않는다** (사용자 수정 보호).
847
+ 바인딩을 재생성하려면:
848
+
849
+ ```sh
850
+ # 특정 파일만 재생성
851
+ rm src/widgets/switch.gleam
852
+ gleam run -m mendraw/install
853
+
854
+ # 전체 재생성
855
+ rm src/widgets/*.gleam
856
+ gleam run -m mendraw/install
857
+ ```
858
+
859
+ ### "widget_ffi.mjs not generated" 에러
860
+
861
+ `widget_ffi.mjs`와 `classic_ffi.mjs`는 스텁 파일로 시작한다.
862
+ `gleam run -m mendraw/install`을 실행하면 빌드 경로에 실제 파일이 생성된다.
863
+ install을 실행하지 않고 위젯 모듈을 import하면 이 에러가 발생한다.
864
+
321
865
  ### Classic 위젯이 렌더링되지 않는다
322
866
 
323
867
  - Classic 위젯은 DOM 컨테이너를 생성하고 imperative하게 마운트한다
324
868
  - `classic_ffi.mjs`가 빌드 경로에 정상적으로 생성되었는지 확인
325
869
  - `widget_id`가 정확한지 확인 (예: `"CameraWidget.widget.CameraWidget"`)
326
870
 
871
+ ### Synthetic 데이터 차트에서 `toNumber is not a function` 에러
872
+
873
+ 차트 위젯은 Decimal 값에 Big.js의 `.toNumber()` 메서드를 호출한다.
874
+ `list_attribute`의 `attr_type` 파라미터가 `"Decimal"`, `"Integer"`, `"Long"` 중 하나인지 확인한다.
875
+ 해당 타입으로 지정하면 synthetic 모듈이 자동으로 Big.js 호환 객체로 래핑한다.
876
+
877
+ ### Synthetic 데이터 차트에서 `get is not a function` 에러
878
+
879
+ 차트 위젯이 `staticName` 등 textTemplate 속성에서 `.get(item)` 메서드를 호출한다.
880
+ `synthetic.text_template()`은 `.get()` 메서드를 포함하므로 정상 동작해야 한다.
881
+ 커스텀 JS 객체를 직접 전달하는 경우 `.get(item)` 메서드가 있는지 확인한다.
882
+
883
+ ### Chart 위젯의 shared-charts.mjs 경로 에러
884
+
885
+ `Could not resolve "../../../shared/charts/esm/shared-charts.mjs"` 에러가 발생하면
886
+ `gleam run -m mendraw/install`을 다시 실행한다. mendraw가 자동으로 공유 의존성 파일을 복사하고 import 경로를 재작성한다.
887
+
888
+ ### Pluggable 위젯과 Classic 위젯을 구분하는 기준
889
+
890
+ mendraw는 `.mpk` 파일 내부의 구조로 자동 판별한다:
891
+
892
+ - `.mjs` 파일이 포함되어 있으면 → **Pluggable** 위젯
893
+ - `.mjs` 없이 `.js`만 포함되어 있으면 → **Classic** 위젯
327
894
 
328
895
  사용자가 별도로 지정할 필요 없다.
@@ -8,8 +8,8 @@ runtime = "node"
8
8
 
9
9
  [dependencies]
10
10
  gleam_stdlib = ">= 0.44.0 and < 2.0.0"
11
- glendix = ">= 4.0.3 and < 5.0.0"
12
- mendraw = ">= 1.1.11 and < 2.0.0"
11
+ glendix = ">= 4.0.4 and < 5.0.0"
12
+ mendraw = ">= 1.2.1 and < 2.0.0"
13
13
  dee = ">= 1.0.0 and < 2.0.0"
14
14
  redraw = ">= 19.2.2 and < 20.0.0"
15
15
  redraw_dom = ">= 19.2.2 and < 20.0.0"