create-mendix-widget-gleam 3.0.2 → 4.0.0

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.
@@ -0,0 +1,671 @@
1
+ # mendraw 사용 가이드
2
+
3
+ Mendix 위젯 `.mpk` 파일에서 Gleam/[redraw](https://hexdocs.pm/redraw/) 바인딩을 자동 생성하는 라이브러리.
4
+ Pluggable(React) 위젯과 Classic(Dojo) 위젯을 모두 지원한다.
5
+
6
+ ---
7
+
8
+ ## 목차
9
+
10
+ - [사전 준비](#사전-준비)
11
+ - [설치](#설치)
12
+ - [빠른 시작](#빠른-시작)
13
+ - [Marketplace에서 위젯 다운로드](#marketplace에서-위젯-다운로드)
14
+ - [생성된 바인딩 사용하기](#생성된-바인딩-사용하기)
15
+ - [Pluggable 위젯](#pluggable-위젯)
16
+ - [Classic (Dojo) 위젯](#classic-dojo-위젯)
17
+ - [저수준 API로 직접 조립하기](#저수준-api로-직접-조립하기)
18
+ - [위젯 컴포넌트 조회](#위젯-컴포넌트-조회)
19
+ - [Prop 래핑](#prop-래핑)
20
+ - [컴포넌트 렌더링](#컴포넌트-렌더링)
21
+ - [JsProps 다루기](#jsprops-다루기)
22
+ - [Prop 접근자](#prop-접근자)
23
+ - [ValueStatus](#valuestatus)
24
+ - [Option 변환](#option-변환)
25
+ - [생성된 코드 구조](#생성된-코드-구조)
26
+ - [Pluggable 위젯 바인딩 예시](#pluggable-위젯-바인딩-예시)
27
+ - [Classic 위젯 바인딩 예시](#classic-위젯-바인딩-예시)
28
+ - [파일명 변환 규칙](#파일명-변환-규칙)
29
+ - [생성된 바인딩 커스터마이징](#생성된-바인딩-커스터마이징)
30
+ - [API 레퍼런스](#api-레퍼런스)
31
+ - [mendraw/mendix](#mendrawmendix)
32
+ - [mendraw/widget](#mendrawwidget)
33
+ - [mendraw/interop](#mendrawinterop)
34
+ - [mendraw/classic](#mendrawclassic)
35
+ - [mendraw/marketplace](#mendrawmarketplace)
36
+ - [glendix 프로젝트에서 사용하기](#glendix-프로젝트에서-사용하기)
37
+ - [문제 해결](#문제-해결)
38
+
39
+ ---
40
+
41
+ ## 사전 준비
42
+
43
+ - [Gleam](https://gleam.run/) v1.15 이상
44
+ - [Erlang/OTP](https://www.erlang.org/) 28 이상
45
+ - Gleam 프로젝트의 타겟이 `javascript`여야 한다 (`gleam.toml`의 `target = "javascript"`)
46
+ - [redraw](https://hexdocs.pm/redraw/) 기반 UI 프로젝트
47
+
48
+ ---
49
+
50
+ ## 설치
51
+
52
+ ```sh
53
+ gleam add mendraw@1
54
+ ```
55
+
56
+ `gleam.toml`에 다음 의존성이 추가된다:
57
+
58
+ ```toml
59
+ [dependencies]
60
+ mendraw = ">= 1.1.10 and < 2.0.0"
61
+ ```
62
+
63
+ mendraw는 `gleam_stdlib`, `gleam_javascript`, `redraw`, `redraw_dom`을 함께 가져온다.
64
+ 이미 프로젝트에 이들이 있다면 버전 호환성만 확인하면 된다.
65
+
66
+ ---
67
+
68
+ ## 빠른 시작
69
+
70
+ ### 1단계: 위젯 등록
71
+
72
+ **`gleam.toml`로 자동 다운로드**
73
+
74
+ ```toml
75
+ [tools.mendraw.widgets.Charts]
76
+ version = "3.0.0"
77
+ # s3_id = "com/..." ← 있으면 인증 없이 직접 다운로드
78
+ ```
79
+
80
+ `gleam run -m mendraw/install` 실행 시 `build/widgets/`에 캐시하고 바인딩을 자동 생성한다.
81
+ Marketplace TUI(`gleam run -m mendraw/marketplace`)에서 다운로드하면 gleam.toml에 자동 추가된다.
82
+
83
+ ### 2단계: 바인딩 생성
84
+
85
+ ```sh
86
+ gleam run -m mendraw/install
87
+ ```
88
+
89
+ 실행 결과:
90
+
91
+ - TOML에 등록된 위젯을 `build/widgets/`에 다운로드/캐시
92
+ - `src/widgets/`에 위젯별 `.gleam` 바인딩 파일이 생성된다
93
+ - 빌드 경로에 `widget_ffi.mjs`(컴포넌트 레지스트리)가 생성된다
94
+ - Classic 위젯이 있으면 `classic_ffi.mjs`(런타임)도 생성된다
95
+
96
+ ```
97
+ src/widgets/
98
+ ├── switch.gleam ← Pluggable 위젯
99
+ ├── area_chart.gleam ← Charts.mpk에서 추출
100
+ ├── bar_chart.gleam
101
+ └── camera_widget.gleam ← Classic 위젯
102
+ ```
103
+
104
+ ### 3단계: 바인딩 사용
105
+
106
+ ```gleam
107
+ import widgets/switch
108
+ import mendraw/mendix.{type JsProps}
109
+ import redraw.{type Element}
110
+
111
+ pub fn my_view(props: JsProps) -> Element {
112
+ switch.render(props)
113
+ }
114
+ ```
115
+
116
+ 이것으로 끝이다. 아래에서 각 단계를 자세히 설명한다.
117
+
118
+ ---
119
+
120
+ ## Marketplace에서 위젯 다운로드
121
+
122
+ `.mpk` 파일을 직접 구하는 대신, Mendix Marketplace에서 위젯을 검색하고 다운로드할 수 있는 TUI를 제공한다.
123
+
124
+ ### 사전 설정
125
+
126
+ `.env` 파일에 Mendix Personal Access Token을 설정한다:
127
+
128
+ ```
129
+ MENDIX_PAT=your_personal_access_token
130
+ ```
131
+
132
+ PAT는 Mendix Portal → Settings → Personal Access Tokens에서 발급한다.
133
+ 필요한 scope: `mx:marketplace-content:read`
134
+
135
+ ### 실행
136
+
137
+ ```sh
138
+ gleam run -m mendraw/marketplace
139
+ ```
140
+
141
+ ### TUI 모드 (터미널)
142
+
143
+ 터미널에서 실행하면 인터랙티브 TUI가 표시된다:
144
+
145
+ | 키 | 동작 |
146
+ |---|---|
147
+ | `↑` `↓` | 위젯 목록 이동 |
148
+ | `←` `→` | 페이지 이동 |
149
+ | `Space` | 위젯 선택/해제 (복수 선택) |
150
+ | `Enter` | 선택한 위젯 다운로드 → 버전 선택 화면 |
151
+ | `Esc` | 검색/선택 초기화 |
152
+ | 문자 입력 | 이름/퍼블리셔 검색 |
153
+ | `q` | 종료 |
154
+
155
+ 버전 선택 화면에서:
156
+
157
+ | 키 | 동작 |
158
+ |---|---|
159
+ | `↑` `↓` | 버전 이동 |
160
+ | `Enter` | 선택한 버전 다운로드 |
161
+ | `Esc` | 목록으로 돌아가기 |
162
+
163
+ ### 프롬프트 모드 (비-TTY)
164
+
165
+ 파이프 등 비-TTY 환경에서는 텍스트 프롬프트 모드로 동작한다:
166
+
167
+ ```
168
+ > 0 ← 0번 위젯 다운로드
169
+ > 0,3,5 ← 여러 위젯 동시 선택
170
+ > switch ← "switch" 검색
171
+ > n ← 다음 페이지
172
+ > p ← 이전 페이지
173
+ > r ← 검색 초기화
174
+ > q ← 종료
175
+ ```
176
+
177
+ ### 다운로드 후
178
+
179
+ 다운로드한 위젯은 `build/widgets/`에 캐시되고 `gleam.toml`의 `[tools.mendraw.widgets.*]`에 자동 추가된다.
180
+ 다운로드가 1개 이상 완료되면 자동으로 `cmd.generate_widget_bindings()`가 실행되어 바인딩이 생성된다.
181
+
182
+ > **참고**: 첫 다운로드 시 chrobot_extra 사이드카를 통한 Mendix 로그인이 필요할 수 있다.
183
+ > 로그인 세션은 `.marketplace-cache/session.json`에 캐시된다.
184
+ > 사이드카가 처음 실행될 때 자동으로 설정된다 (Erlang/OTP가 필요).
185
+
186
+ ---
187
+
188
+ ## 생성된 바인딩 사용하기
189
+
190
+ `gleam run -m mendraw/install`이 생성하는 `.gleam` 파일에는 `render` 함수가 포함된다.
191
+ 이 함수는 Mendix가 전달하는 `JsProps`를 받아 `redraw` `Element`를 반환한다.
192
+
193
+ ### Pluggable 위젯
194
+
195
+ Pluggable 위젯(React 기반)의 생성된 바인딩:
196
+
197
+ ```gleam
198
+ import widgets/switch
199
+ import mendraw/mendix.{type JsProps}
200
+ import redraw.{type Element}
201
+
202
+ /// Mendix가 위젯에 전달하는 props를 그대로 넘긴다
203
+ pub fn view(props: JsProps) -> Element {
204
+ switch.render(props)
205
+ }
206
+ ```
207
+
208
+ 생성된 `render` 함수는 내부적으로:
209
+ 1. `mendix.get_prop_required`/`mendix.get_prop`으로 props에서 속성을 추출
210
+ 2. `widget.component`로 원본 React 컴포넌트를 가져옴
211
+ 3. `interop.component_el`로 redraw Element를 생성
212
+
213
+ ### Classic (Dojo) 위젯
214
+
215
+ Classic 위젯(Dojo 기반)의 생성된 바인딩:
216
+
217
+ ```gleam
218
+ import widgets/camera_widget
219
+ import mendraw/mendix.{type JsProps}
220
+ import redraw.{type Element}
221
+
222
+ pub fn view(props: JsProps) -> Element {
223
+ camera_widget.render(props)
224
+ }
225
+ ```
226
+
227
+ Classic 위젯은 내부적으로 DOM 컨테이너를 생성하고, `useEffect`로 위젯의 마운트/언마운트를 관리한다.
228
+
229
+ ---
230
+
231
+ ## 저수준 API로 직접 조립하기
232
+
233
+ 생성된 바인딩 대신, `mendraw/widget`과 `mendraw/interop` 모듈을 직접 사용하여
234
+ 위젯 렌더링을 세밀하게 제어할 수 있다.
235
+
236
+ ### 위젯 컴포넌트 조회
237
+
238
+ ```gleam
239
+ import mendraw/widget
240
+
241
+ // 위젯 이름으로 React 컴포넌트를 가져온다
242
+ let comp = widget.component("Switch")
243
+ ```
244
+
245
+ 위젯 이름은 `.mpk` 파일의 `widget.xml`에 정의된 `<name>` 값이다.
246
+
247
+ ### Prop 래핑
248
+
249
+ Mendix 위젯은 일반 값이 아닌 래핑된 값 객체를 기대한다.
250
+ mendraw는 세 가지 prop 래퍼를 제공한다:
251
+
252
+ ```gleam
253
+ import mendraw/widget
254
+
255
+ // 1. 읽기 전용 prop (DynamicValue)
256
+ // expression, textTemplate 등 읽기 전용 속성에 사용
257
+ widget.prop("caption", "제목 텍스트")
258
+
259
+ // 2. 편집 가능한 prop (EditableValue)
260
+ // 사용자 입력이 필요한 속성에 사용
261
+ widget.editable_prop("textAttr", current_value, "표시값", fn(new_val) {
262
+ // 값이 변경되었을 때의 처리
263
+ Nil
264
+ })
265
+
266
+ // 3. 액션 prop (ActionValue)
267
+ // onClick, onLeave 등 이벤트 핸들러에 사용
268
+ widget.action_prop("onClick", fn() {
269
+ // 클릭 시 처리
270
+ Nil
271
+ })
272
+ ```
273
+
274
+ 각 래퍼가 생성하는 Mendix 값 객체:
275
+
276
+ | 래퍼 | Mendix 타입 | 구조 |
277
+ |------|------------|------|
278
+ | `prop` | `DynamicValue` | `{ status: "available", value }` |
279
+ | `editable_prop` | `EditableValue` | `{ status: "available", value, displayValue, readOnly: false, setValue, ... }` |
280
+ | `action_prop` | `ActionValue` | `{ canExecute: true, isExecuting: false, execute }` |
281
+
282
+ ### 컴포넌트 렌더링
283
+
284
+ ```gleam
285
+ import mendraw/interop
286
+ import mendraw/widget
287
+ import redraw/dom/attribute
288
+
289
+ let comp = widget.component("Switch")
290
+
291
+ // 속성 + 자식 엘리먼트
292
+ interop.component_el(comp, [
293
+ widget.prop("caption", "제목"),
294
+ widget.editable_prop("textAttr", value, "표시값", set_value),
295
+ widget.action_prop("onClick", handler),
296
+ ], [])
297
+
298
+ // 속성 없이 자식만
299
+ interop.component_el_(comp, [child1, child2])
300
+
301
+ // 자식 없는 self-closing 컴포넌트
302
+ interop.void_component_el(comp, [
303
+ widget.prop("caption", "읽기 전용"),
304
+ ])
305
+ ```
306
+
307
+ ### Classic 위젯 직접 렌더링
308
+
309
+ ```gleam
310
+ import mendraw/classic
311
+
312
+ // 기본 렌더링
313
+ classic.render("CameraWidget.widget.CameraWidget", [
314
+ #("mfToExecute", classic.to_dynamic(mf_value)),
315
+ #("preferRearCamera", classic.to_dynamic(True)),
316
+ ])
317
+
318
+ // CSS 클래스 지정
319
+ classic.render_with_class(
320
+ "CameraWidget.widget.CameraWidget",
321
+ [#("mfToExecute", classic.to_dynamic(mf_value))],
322
+ "my-camera-wrapper",
323
+ )
324
+ ```
325
+
326
+ `widget_id`는 Classic 위젯의 정규화된 ID이다 (예: `"CameraWidget.widget.CameraWidget"`).
327
+
328
+ ---
329
+
330
+ ## JsProps 다루기
331
+
332
+ Mendix 런타임이 위젯에 전달하는 props 객체를 `JsProps` 타입으로 다룬다.
333
+
334
+ ### Prop 접근자
335
+
336
+ ```gleam
337
+ import mendraw/mendix.{type JsProps}
338
+ import gleam/option.{type Option, None, Some}
339
+
340
+ fn handle_props(props: JsProps) {
341
+ // 필수 속성 — 항상 존재한다고 가정
342
+ let name: String = mendix.get_prop_required(props, "name")
343
+
344
+ // 선택 속성 — 없으면 None
345
+ let caption: Option(String) = mendix.get_prop(props, "caption")
346
+
347
+ // 문자열 속성 — 없으면 빈 문자열 ""
348
+ let label: String = mendix.get_string_prop(props, "label")
349
+
350
+ // 속성 존재 여부 확인
351
+ let has_icon: Bool = mendix.has_prop(props, "icon")
352
+ }
353
+ ```
354
+
355
+ | 함수 | 반환 타입 | 없을 때 |
356
+ |------|----------|---------|
357
+ | `get_prop_required(props, key)` | `a` | 런타임 에러 |
358
+ | `get_prop(props, key)` | `Option(a)` | `None` |
359
+ | `get_string_prop(props, key)` | `String` | `""` |
360
+ | `has_prop(props, key)` | `Bool` | `False` |
361
+
362
+ ### ValueStatus
363
+
364
+ Mendix의 `DynamicValue`, `EditableValue` 등은 `status` 속성을 가진다.
365
+ 데이터 로딩 상태를 확인할 때 사용한다:
366
+
367
+ ```gleam
368
+ import mendraw/mendix
369
+
370
+ let value = mendix.get_prop_required(props, "textAttr")
371
+
372
+ case mendix.get_status(value) {
373
+ mendix.Available -> // 값 사용 가능
374
+ use_value(value)
375
+ mendix.Loading -> // 로딩 중
376
+ show_spinner()
377
+ mendix.Unavailable -> // 사용 불가
378
+ show_placeholder()
379
+ }
380
+ ```
381
+
382
+ ### Option 변환
383
+
384
+ JS/Gleam 경계에서 `undefined`/`null`을 안전하게 처리한다:
385
+
386
+ ```gleam
387
+ import mendraw/mendix
388
+
389
+ // JS undefined/null → Gleam None, 값이 있으면 Some(값)
390
+ let maybe_value = mendix.to_option(js_value)
391
+
392
+ // Gleam Option → JS 값 (None → undefined)
393
+ let js_value = mendix.from_option(gleam_option)
394
+ ```
395
+
396
+ ---
397
+
398
+ ## 생성된 코드 구조
399
+
400
+ ### Pluggable 위젯 바인딩 예시
401
+
402
+ Switch 위젯에서 생성되는 `src/widgets/switch.gleam`:
403
+
404
+ ```gleam
405
+ // @generated mendraw/install — 직접 수정 금지
406
+
407
+ import gleam/option.{None, Some}
408
+ import mendraw/interop
409
+ import mendraw/mendix.{type JsProps}
410
+ import mendraw/widget
411
+ import redraw.{type Element}
412
+ import redraw/dom/attribute
413
+
414
+ pub fn render(props: JsProps) -> Element {
415
+ // 필수 속성
416
+ let text_attr = mendix.get_prop_required(props, "textAttr")
417
+ // 선택 속성
418
+ let caption = mendix.get_prop(props, "caption")
419
+
420
+ let comp = widget.component("Switch")
421
+ interop.component_el(
422
+ comp,
423
+ [
424
+ attribute.attribute("textAttr", text_attr),
425
+ // 선택 속성은 있을 때만 전달
426
+ ..optional_attr("caption", caption)
427
+ ],
428
+ [],
429
+ )
430
+ }
431
+
432
+ fn optional_attr(key: String, value: option.Option(a)) -> List(attribute.Attribute) {
433
+ case value {
434
+ Some(v) -> [attribute.attribute(key, v)]
435
+ None -> []
436
+ }
437
+ }
438
+ ```
439
+
440
+ ### Classic 위젯 바인딩 예시
441
+
442
+ CameraWidget 위젯에서 생성되는 `src/widgets/camera_widget.gleam`:
443
+
444
+ ```gleam
445
+ // @generated mendraw/install — 직접 수정 금지
446
+
447
+ import gleam/option.{None, Some}
448
+ import mendraw/classic
449
+ import mendraw/mendix.{type JsProps}
450
+ import redraw.{type Element}
451
+
452
+ pub fn render(props: JsProps) -> Element {
453
+ let mf_to_execute = mendix.get_prop_required(props, "mfToExecute")
454
+ let prefer_rear_camera = mendix.get_prop_required(props, "preferRearCamera")
455
+
456
+ classic.render("CameraWidget.widget.CameraWidget", [
457
+ #("mfToExecute", classic.to_dynamic(mf_to_execute)),
458
+ #("preferRearCamera", classic.to_dynamic(prefer_rear_camera)),
459
+ ])
460
+ }
461
+ ```
462
+
463
+ ### 파일명 변환 규칙
464
+
465
+ 위젯 이름은 Gleam 모듈명 규칙에 맞게 snake_case로 변환된다:
466
+
467
+ | 위젯 이름 | 파일명 | 모듈 경로 |
468
+ |-----------|--------|----------|
469
+ | `Switch` | `switch.gleam` | `widgets/switch` |
470
+ | `AreaChart` | `area_chart.gleam` | `widgets/area_chart` |
471
+ | `BarChart` | `bar_chart.gleam` | `widgets/bar_chart` |
472
+ | `CameraWidget` | `camera_widget.gleam` | `widgets/camera_widget` |
473
+ | `Progress Bar` | `progress_bar.gleam` | `widgets/progress_bar` |
474
+
475
+ ---
476
+
477
+ ## 생성된 바인딩 커스터마이징
478
+
479
+ 생성된 `src/widgets/*.gleam` 파일은 한 번 생성된 후 **덮어쓰지 않는다**.
480
+ 따라서 생성된 파일을 직접 수정하여 커스터마이징할 수 있다:
481
+
482
+ ```gleam
483
+ // src/widgets/switch.gleam — 사용자가 수정한 버전
484
+ import mendraw/interop
485
+ import mendraw/mendix.{type JsProps}
486
+ import mendraw/widget
487
+ import redraw.{type Element}
488
+ import redraw/dom/attribute
489
+
490
+ pub fn render(props: JsProps) -> Element {
491
+ let text_attr = mendix.get_prop_required(props, "textAttr")
492
+ let comp = widget.component("Switch")
493
+ interop.component_el(
494
+ comp,
495
+ [
496
+ widget.editable_prop("textAttr", text_attr, "표시값", fn(v) { Nil }),
497
+ // 커스텀: 고정 캡션 추가
498
+ widget.prop("caption", "내 스위치"),
499
+ ],
500
+ [],
501
+ )
502
+ }
503
+ ```
504
+
505
+ > **주의**: 수정한 파일은 `gleam run -m mendraw/install`을 다시 실행해도 덮어쓰지 않는다.
506
+ > 바인딩을 재생성하려면 해당 파일을 삭제한 후 install을 다시 실행한다.
507
+
508
+ ---
509
+
510
+ ## API 레퍼런스
511
+
512
+ ### mendraw/mendix
513
+
514
+ Mendix Pluggable Widget API의 핵심 타입과 props 접근자.
515
+
516
+ #### 타입
517
+
518
+ | 타입 | 설명 |
519
+ |------|------|
520
+ | `JsProps` | Mendix가 위젯에 전달하는 props 객체 (opaque) |
521
+ | `ValueStatus` | `Available \| Unavailable \| Loading` |
522
+ | `ObjectItem` | Mendix 데이터 객체 (opaque) |
523
+
524
+ #### 함수
525
+
526
+ | 함수 | 시그니처 | 설명 |
527
+ |------|----------|------|
528
+ | `get_prop` | `(JsProps, String) -> Option(a)` | 선택 속성 추출 (없으면 `None`) |
529
+ | `get_prop_required` | `(JsProps, String) -> a` | 필수 속성 추출 |
530
+ | `get_string_prop` | `(JsProps, String) -> String` | 문자열 속성 (없으면 `""`) |
531
+ | `has_prop` | `(JsProps, String) -> Bool` | 속성 존재 여부 |
532
+ | `get_status` | `(a) -> ValueStatus` | 값 객체의 로딩 상태 |
533
+ | `object_id` | `(ObjectItem) -> String` | 데이터 객체 ID |
534
+ | `to_value_status` | `(String) -> ValueStatus` | 문자열 → `ValueStatus` 변환 |
535
+ | `to_option` | `(a) -> Option(a)` | JS undefined/null → `None` |
536
+ | `from_option` | `(Option(a)) -> a` | Gleam `Option` → JS 값 (`None` → undefined) |
537
+
538
+ ### mendraw/widget
539
+
540
+ Pluggable 위젯 컴포넌트 조회 및 prop 래핑.
541
+
542
+ | 함수 | 시그니처 | 설명 |
543
+ |------|----------|------|
544
+ | `component` | `(String) -> JsComponent` | 이름으로 위젯 컴포넌트 조회 |
545
+ | `prop` | `(String, a) -> Attribute` | `DynamicValue`로 래핑 (읽기 전용) |
546
+ | `editable_prop` | `(String, a, String, fn(a) -> Nil) -> Attribute` | `EditableValue`로 래핑 (편집 가능) |
547
+ | `action_prop` | `(String, fn() -> Nil) -> Attribute` | `ActionValue`로 래핑 (이벤트 핸들러) |
548
+
549
+ ### mendraw/interop
550
+
551
+ 외부 JS React 컴포넌트를 redraw Element로 변환하는 브릿지.
552
+
553
+ | 함수 | 시그니처 | 설명 |
554
+ |------|----------|------|
555
+ | `component_el` | `(JsComponent, List(Attribute), List(Element)) -> Element` | 속성 + 자식으로 렌더링 |
556
+ | `component_el_` | `(JsComponent, List(Element)) -> Element` | 자식만으로 렌더링 |
557
+ | `void_component_el` | `(JsComponent, List(Attribute)) -> Element` | self-closing 렌더링 |
558
+
559
+ #### 타입
560
+
561
+ | 타입 | 설명 |
562
+ |------|------|
563
+ | `JsComponent` | 외부 React 컴포넌트 참조 (opaque) |
564
+
565
+ ### mendraw/classic
566
+
567
+ Classic (Dojo) 위젯을 React 내부에서 렌더링.
568
+
569
+ | 함수 | 시그니처 | 설명 |
570
+ |------|----------|------|
571
+ | `render` | `(String, List(#(String, Dynamic))) -> Element` | Classic 위젯 렌더링 |
572
+ | `render_with_class` | `(String, List(#(String, Dynamic)), String) -> Element` | CSS 클래스 지정 렌더링 |
573
+ | `to_dynamic` | `(a) -> Dynamic` | 값을 `Dynamic`으로 변환 |
574
+
575
+ ### mendraw/cmd
576
+
577
+ 위젯 바인딩 생성 + TOML 위젯 관리 API.
578
+
579
+ | 함수 | 시그니처 | 설명 |
580
+ |------|----------|------|
581
+ | `file_exists` | `(String) -> Bool` | 파일 존재 여부 |
582
+ | `generate_widget_bindings` | `() -> Nil` | build/widgets/ 캐시에서 바인딩 생성 |
583
+ | `resolve_toml_widgets` | `() -> Nil` | gleam.toml [tools.mendraw.widgets.*] 다운로드/캐시 |
584
+ | `write_widget_toml` | `(String, String, Option(Int), Option(String)) -> Nil` | gleam.toml에 위젯 항목 쓰기 |
585
+ | `download_to_cache` | `(String, String, String, Option(Int)) -> Bool` | URL에서 build/widgets/{name}/에 다운로드+추출 |
586
+
587
+ ### mendraw/marketplace
588
+
589
+ Mendix Marketplace 위젯 검색·다운로드 TUI. `gleam run -m mendraw/marketplace`로 실행한다.
590
+
591
+ #### 주요 기능
592
+
593
+ | 기능 | 설명 |
594
+ |------|------|
595
+ | 위젯 검색 | 이름/퍼블리셔로 실시간 필터링 |
596
+ | 백그라운드 로딩 | 전체 위젯 목록을 백그라운드에서 점진적으로 로드 |
597
+ | 버전 선택 | Content API + chrobot_extra 사이드카(XAS)로 버전별 다운로드 정보 조회 |
598
+ | 자동 TOML 기록 | 다운로드 시 gleam.toml에 위젯 항목 자동 추가 |
599
+ | 캐시 다운로드 | build/widgets/에 캐시 (소스 컨트롤에 .mpk 불필요) |
600
+ | 자동 바인딩 | 다운로드 완료 후 `generate_widget_bindings()` 자동 호출 |
601
+
602
+ #### 의존성
603
+
604
+ - `etch` — 터미널 raw mode, 커서 제어, ANSI 스타일링
605
+ - `chrobot_extra` — Mendix 로그인 세션 관리, 버전 다운로드 정보 추출 (HTTP 사이드카, Erlang 타겟)
606
+ - `curl` — Content API 호출 (시스템 명령)
607
+
608
+ ---
609
+
610
+ ## glendix 프로젝트에서 사용하기
611
+
612
+ [glendix](https://github.com/) 프로젝트에서 mendraw를 의존성으로 추가하면,
613
+ MPK 바인딩 생성을 mendraw에 위임할 수 있다:
614
+
615
+ ```gleam
616
+ // glendix의 install.gleam
617
+ import mendraw/cmd as mendraw_cmd
618
+
619
+ pub fn main() {
620
+ cmd.exec(cmd.detect_install_command())
621
+ cmd.generate_bindings()
622
+ // MPK 위젯 바인딩 생성을 mendraw에 위임
623
+ mendraw_cmd.generate_widget_bindings()
624
+ }
625
+ ```
626
+
627
+ ---
628
+
629
+ ## 문제 해결
630
+
631
+ ### `gleam run -m mendraw/install` 실행 시 아무것도 생성되지 않는다
632
+
633
+ - `gleam.toml`에 `[tools.mendraw.widgets.*]` 섹션이 있는지 확인
634
+ - `.mpk` 파일이 유효한 ZIP 형식인지 확인
635
+ - 콘솔 출력을 확인하여 파싱 오류가 없는지 점검
636
+
637
+ ### 이미 존재하는 바인딩 파일이 업데이트되지 않는다
638
+
639
+ mendraw는 `src/widgets/`에 이미 존재하는 `.gleam` 파일을 **덮어쓰지 않는다** (사용자 수정 보호).
640
+ 바인딩을 재생성하려면:
641
+
642
+ ```sh
643
+ # 특정 파일만 재생성
644
+ rm src/widgets/switch.gleam
645
+ gleam run -m mendraw/install
646
+
647
+ # 전체 재생성
648
+ rm src/widgets/*.gleam
649
+ gleam run -m mendraw/install
650
+ ```
651
+
652
+ ### "widget_ffi.mjs not generated" 에러
653
+
654
+ `widget_ffi.mjs`와 `classic_ffi.mjs`는 스텁 파일로 시작한다.
655
+ `gleam run -m mendraw/install`을 실행하면 빌드 경로에 실제 파일이 생성된다.
656
+ install을 실행하지 않고 위젯 모듈을 import하면 이 에러가 발생한다.
657
+
658
+ ### Classic 위젯이 렌더링되지 않는다
659
+
660
+ - Classic 위젯은 DOM 컨테이너를 생성하고 imperative하게 마운트한다
661
+ - `classic_ffi.mjs`가 빌드 경로에 정상적으로 생성되었는지 확인
662
+ - `widget_id`가 정확한지 확인 (예: `"CameraWidget.widget.CameraWidget"`)
663
+
664
+ ### Pluggable 위젯과 Classic 위젯을 구분하는 기준
665
+
666
+ mendraw는 `.mpk` 파일 내부의 구조로 자동 판별한다:
667
+
668
+ - `.mjs` 파일이 포함되어 있으면 → **Pluggable** 위젯
669
+ - `.mjs` 없이 `.js`만 포함되어 있으면 → **Classic** 위젯
670
+
671
+ 사용자가 별도로 지정할 필요 없다.
@@ -8,7 +8,9 @@ runtime = "node"
8
8
 
9
9
  [dependencies]
10
10
  gleam_stdlib = ">= 0.44.0 and < 2.0.0"
11
- glendix = ">= 3.0.2 and < 4.0.0"
11
+ glendix = ">= 4.0.2 and < 5.0.0"
12
+ mendraw = ">= 1.1.10 and < 2.0.0"
13
+ dee = ">= 1.0.0 and < 2.0.0"
12
14
  redraw = ">= 19.2.2 and < 20.0.0"
13
15
  redraw_dom = ">= 19.2.2 and < 20.0.0"
14
16
  lustre = ">= 5.6.0 and < 6.0.0"
@@ -19,12 +19,7 @@
19
19
  "prerelease": "echo skipping lint"
20
20
  },
21
21
  "devDependencies": {
22
- "@mendix/pluggable-widgets-tools": "^11.8.0",
23
- "playwright": "^1.58.2"
24
- },
25
- "dependencies": {
26
- "classnames": "^2.5.1",
27
- "big.js": "^6.0.0"
22
+ "@mendix/pluggable-widgets-tools": "^11.8.0"
28
23
  },
29
24
  "resolutions": {
30
25
  "react": "19.0.0",
@@ -2,7 +2,7 @@
2
2
  {{I18N:widget_main_2}}
3
3
 
4
4
  import components/hello_world
5
- import glendix/mendix.{type JsProps}
5
+ import mendraw/mendix.{type JsProps}
6
6
  import redraw.{type Element}
7
7
 
8
8
  {{I18N:widget_main_doc}}