@wipco/sdui 0.9.1 → 0.11.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.
@@ -1,4 +1,4 @@
1
- import { includesLooseSearch } from './chunk-NIRR6KSQ.js';
1
+ import { includesLooseSearch } from './chunk-BJDWMECO.js';
2
2
  import { Icon, Button, Input, Spinner } from './chunk-B5JTXYDB.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import { useId, useRef, useState, useEffect, useMemo } from 'react';
@@ -225,15 +225,18 @@ function SearchToolbar({
225
225
  };
226
226
  return /* @__PURE__ */ jsxs("form", { className: ["wp-row", className].filter(Boolean).join(" "), onSubmit: submit, children: [
227
227
  activeCriteria.length > 0 ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: { flexWrap: "wrap" }, children: activeCriteria.map((criterion) => /* @__PURE__ */ jsx(Tag, { onRemove: () => removeCriterion(criterion), children: criterion.label }, criterion.key)) }) : null,
228
- !hideSearchInput ? /* @__PURE__ */ jsx(
229
- Input,
230
- {
231
- icon: "search",
232
- value: search,
233
- onChange: (event) => onSearchChange(event.target.value),
234
- placeholder
235
- }
236
- ) : null,
228
+ !hideSearchInput ? /* @__PURE__ */ jsxs(Fragment, { children: [
229
+ /* @__PURE__ */ jsx(
230
+ Input,
231
+ {
232
+ icon: "search",
233
+ value: search,
234
+ onChange: (event) => onSearchChange(event.target.value),
235
+ placeholder
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsx(Button, { type: "submit", variant: "secondary", icon: "search", children: "\uAC80\uC0C9" })
239
+ ] }) : null,
237
240
  hasAdvanced ? /* @__PURE__ */ jsx(
238
241
  Popover,
239
242
  {
@@ -45,6 +45,7 @@ function describeParseError(err, text) {
45
45
  function JsonFieldImpl({
46
46
  value,
47
47
  onChange,
48
+ emptyAsNull = false,
48
49
  rows = 6,
49
50
  invalid = false,
50
51
  disabled = false,
@@ -67,6 +68,10 @@ function JsonFieldImpl({
67
68
  focusedRef.current = false;
68
69
  if (text.trim() === "") {
69
70
  setParseError(null);
71
+ if (emptyAsNull) {
72
+ lastFormattedRef.current = "";
73
+ onChange?.(null);
74
+ }
70
75
  return;
71
76
  }
72
77
  try {
@@ -79,7 +84,7 @@ function JsonFieldImpl({
79
84
  } catch (err) {
80
85
  setParseError(describeParseError(err, text));
81
86
  }
82
- }, [text, onChange]);
87
+ }, [text, onChange, emptyAsNull]);
83
88
  return /* @__PURE__ */ jsxs(
84
89
  "div",
85
90
  {
@@ -1,4 +1,4 @@
1
- import { includesLooseSearch } from './chunk-NIRR6KSQ.js';
1
+ import { includesLooseSearch } from './chunk-BJDWMECO.js';
2
2
  import { Button, Dialog, Input, Tag, Checkbox } from './chunk-B5JTXYDB.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import React, { useMemo, useState, useEffect } from 'react';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { CSSProperties, ReactNode, ComponentType } from 'react';
3
- import { S as SduiAction, a as SduiPropValue, b as SduiRule, c as SduiPage } from './types-Dl4rPyan.js';
4
- export { d as SduiCalcRef, e as SduiChild, f as SduiIndexRef, g as SduiItemRef, h as SduiNode, i as SduiPageQuery, j as SduiScalar, k as SduiStateRef } from './types-Dl4rPyan.js';
3
+ import { S as SduiAction, a as SduiPropValue, b as SduiRule, c as SduiPage } from './types-DO8_hC6A.js';
4
+ export { d as SduiCalcRef, e as SduiChild, f as SduiIndexRef, g as SduiItemRef, h as SduiNode, i as SduiPageQuery, j as SduiScalar, k as SduiStateRef } from './types-DO8_hC6A.js';
5
5
  import { S as SduiBoxProps, a as SduiContainerLayoutProps, b as SduiColumnsScale, c as SduiGapScale, d as SduiWeightScale, e as SduiTextAlignScale } from './layout-B1QrIBm6.js';
6
6
  export { C as CHILD_LAYOUT_PROPS, L as LAYOUT_CSS, f as SduiAlignScale, g as SduiChildLayoutProps, h as SduiSizeScale, i as SduiSpanScale, j as childLayoutFrom, k as columnsClass, l as containerLayoutStyle, m as joinClass, n as mergeStyle, t as typographyStyle } from './layout-B1QrIBm6.js';
7
7
  import { D as DatePickerProps, a as DateRangePickerProps } from './DatePicker-BndreCPl.js';
@@ -202,7 +202,8 @@ declare function useRemoteSearchPage(endpoint: string, params: URLSearchParams):
202
202
  * 그대로 옮긴 것. 스펙 호환이 목적이므로 값 규칙(쉼표 다중값, 느슨한 검색, 범위 키 이름)은
203
203
  * 한 글자도 바꾸지 않았다.
204
204
  *
205
- * - 검색 버튼은 두지 않는다(Enter로 검색) — 시안 규칙 유지
205
+ * - 검색 버튼을 입력창과 상세검색 토글 사이에 둔다(Enter 도 그대로 동작) —
206
+ * 원래 시안은 Enter 전용이었는데 실사용 지적으로 명시 버튼을 되돌렸다
206
207
  * - 상세검색은 아이콘 버튼 → 드롭다운 패널(초기화/적용)
207
208
  * - 적용된 조건은 칩(Tag)으로 나열되고, 다중 선택 칩은 값 하나씩 제거된다
208
209
  */
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DEV, HoistedStyle, isSafeNavigationTarget, useSduiAdapter, hostFetchOf, DateRangePicker, DatePicker, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-5A7BWIGN.js';
2
2
  export { DatePicker, DateRangePicker, defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-5A7BWIGN.js';
3
- import { searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-NIRR6KSQ.js';
4
- export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-NIRR6KSQ.js';
3
+ import { searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-BJDWMECO.js';
4
+ export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-BJDWMECO.js';
5
5
  import { EmptyState, Button, Checkbox, SegmentedControl, Spinner, Select, Pagination, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Tabs, Table, Switch, StatCard, Input, IconButton, Field, Alert, warnOnce, Dialog } from './chunk-B5JTXYDB.js';
6
6
  export { Alert, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip } from './chunk-B5JTXYDB.js';
7
7
  import { boxed, mergeStyle, containerLayoutStyle, joinClass, columnsClass, typographyStyle, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-SAN4IX6N.js';
@@ -688,47 +688,52 @@ function SearchResults({
688
688
  }
689
689
  return entries;
690
690
  }, [leftFilters, rightFilters, searchParams]);
691
+ const hasAllRows = item.data.length >= item.total;
691
692
  const processed = useMemo(() => {
692
693
  const cell = (row, key) => row[key];
693
694
  let rows = item.data;
694
- if (appliedSearch) {
695
- rows = rows.filter(
696
- (row) => includesLooseSearch(
697
- Object.values(row).filter((v) => typeof v === "string" || typeof v === "number").join(" "),
698
- appliedSearch
699
- )
700
- );
701
- }
702
- for (const { key, values } of activeFilters) {
703
- rows = rows.filter((row) => values.includes(String(cell(row, key))));
704
- }
705
- for (const field of advancedSearchFields) {
706
- if (field.type === "select") {
707
- const wanted = splitMultiValue(advancedValues[field.key] ?? "");
708
- if (wanted.length > 0) rows = rows.filter((row) => wanted.includes(String(cell(row, field.key))));
709
- } else if (field.type === "cascading") {
710
- const wanted = splitMultiValue(advancedValues[field.key] ?? "");
711
- if (wanted.length > 0) {
712
- const lineages = cascadingValuePaths(field.options);
713
- rows = rows.filter((row) => {
714
- const own = String(cell(row, field.key));
715
- const lineage = lineages.get(own) ?? [own];
716
- return lineage.some((value) => wanted.includes(value));
717
- });
695
+ if (hasAllRows) {
696
+ if (appliedSearch) {
697
+ rows = rows.filter(
698
+ (row) => includesLooseSearch(
699
+ Object.values(row).filter((v) => typeof v === "string" || typeof v === "number").join(" "),
700
+ appliedSearch
701
+ )
702
+ );
703
+ }
704
+ for (const { key, values } of activeFilters) {
705
+ rows = rows.filter((row) => values.includes(String(cell(row, key))));
706
+ }
707
+ for (const field of advancedSearchFields) {
708
+ if (field.type === "select") {
709
+ const wanted = splitMultiValue(advancedValues[field.key] ?? "");
710
+ if (wanted.length > 0) rows = rows.filter((row) => wanted.includes(String(cell(row, field.key))));
711
+ } else if (field.type === "cascading") {
712
+ const wanted = splitMultiValue(advancedValues[field.key] ?? "");
713
+ if (wanted.length > 0) {
714
+ const lineages = cascadingValuePaths(field.options);
715
+ rows = rows.filter((row) => {
716
+ const own = String(cell(row, field.key));
717
+ const lineage = lineages.get(own) ?? [own];
718
+ return lineage.some((value) => wanted.includes(value));
719
+ });
720
+ }
721
+ } else if (field.type === "dateRange") {
722
+ const from = advancedValues[field.fromKey ?? `${field.key}From`];
723
+ const to = advancedValues[field.toKey ?? `${field.key}To`];
724
+ const datePart = (v) => String(v ?? "").slice(0, 10);
725
+ if (from) rows = rows.filter((row) => datePart(cell(row, field.key)) >= from.slice(0, 10));
726
+ if (to) rows = rows.filter((row) => datePart(cell(row, field.key)) <= to.slice(0, 10));
727
+ } else if (field.type === "numberRange") {
728
+ const min = numericValue(advancedValues[field.fromKey ?? `${field.key}Min`]);
729
+ const max = numericValue(advancedValues[field.toKey ?? `${field.key}Max`]);
730
+ if (min !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? -Infinity) >= min);
731
+ if (max !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? Infinity) <= max);
732
+ } else {
733
+ const keyword = advancedValues[field.key];
734
+ const baseKey = field.key.endsWith("Contains") ? field.key.slice(0, -"Contains".length) : field.key;
735
+ if (keyword) rows = rows.filter((row) => includesLooseSearch(String(cell(row, baseKey) ?? ""), keyword));
718
736
  }
719
- } else if (field.type === "dateRange") {
720
- const from = advancedValues[field.fromKey ?? `${field.key}From`];
721
- const to = advancedValues[field.toKey ?? `${field.key}To`];
722
- if (from) rows = rows.filter((row) => String(cell(row, field.key) ?? "") >= from);
723
- if (to) rows = rows.filter((row) => String(cell(row, field.key) ?? "") <= to);
724
- } else if (field.type === "numberRange") {
725
- const min = numericValue(advancedValues[field.fromKey ?? `${field.key}Min`]);
726
- const max = numericValue(advancedValues[field.toKey ?? `${field.key}Max`]);
727
- if (min !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? -Infinity) >= min);
728
- if (max !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? Infinity) <= max);
729
- } else {
730
- const keyword = advancedValues[field.key];
731
- if (keyword) rows = rows.filter((row) => includesLooseSearch(String(cell(row, field.key) ?? ""), keyword));
732
737
  }
733
738
  }
734
739
  if (sort) {
@@ -746,8 +751,8 @@ function SearchResults({
746
751
  });
747
752
  }
748
753
  return rows;
749
- }, [item.data, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
750
- const clientPaginated = item.data.length >= item.total;
754
+ }, [item.data, hasAllRows, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
755
+ const clientPaginated = hasAllRows;
751
756
  const totalCount = clientPaginated ? processed.length : item.total;
752
757
  const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
753
758
  const safePage = Math.min(Math.max(1, page), pageCount);
@@ -1808,8 +1813,11 @@ function validatePageData(data, issues) {
1808
1813
  if (raw.select !== void 0 && typeof raw.select !== "string") {
1809
1814
  issues.push({ path: `${path}.select`, message: "select\uB294 \uBB38\uC790\uC5F4 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1810
1815
  }
1816
+ if (raw.debounceMs !== void 0 && typeof raw.debounceMs !== "number") {
1817
+ issues.push({ path: `${path}.debounceMs`, message: "debounceMs\uB294 \uC22B\uC790(ms)\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1818
+ }
1811
1819
  for (const [k, v] of Object.entries(raw)) {
1812
- if (k === "endpoint" || k === "select") continue;
1820
+ if (k === "endpoint" || k === "select" || k === "debounceMs") continue;
1813
1821
  if (k !== "when" && k !== "fallback") {
1814
1822
  issues.push({ path: `${path}.${k}`, message: `\uC54C \uC218 \uC5C6\uB294 \uD56D\uBAA9: ${k}` });
1815
1823
  continue;
@@ -1960,12 +1968,14 @@ function planPageData(data, resolve) {
1960
1968
  const resolved = resolve(query.endpoint);
1961
1969
  const endpoint = typeof resolved === "string" ? resolved.trim() : "";
1962
1970
  const enabled = "when" in query ? Boolean(resolve(query.when)) : true;
1971
+ const debounceMs = typeof query.debounceMs === "number" && Number.isFinite(query.debounceMs) && query.debounceMs > 0 ? query.debounceMs : void 0;
1963
1972
  plans.push({
1964
1973
  key,
1965
1974
  endpoint,
1966
1975
  enabled,
1967
1976
  select: typeof query.select === "string" ? query.select : void 0,
1968
- fallback: query.fallback
1977
+ fallback: query.fallback,
1978
+ debounceMs
1969
1979
  });
1970
1980
  }
1971
1981
  return plans;
@@ -2458,12 +2468,18 @@ function SduiRenderer({
2458
2468
  if (runnable.length === 0) return;
2459
2469
  const controller = new AbortController();
2460
2470
  let cancelled = false;
2471
+ const timers = [];
2461
2472
  setEntries(
2462
2473
  Object.fromEntries(runnable.map((plan) => [`${plan.key}.$loading`, true]))
2463
2474
  );
2475
+ const afterDebounce = (plan) => plan.debounceMs === void 0 ? Promise.resolve() : new Promise((resolveWait) => {
2476
+ timers.push(setTimeout(resolveWait, plan.debounceMs));
2477
+ });
2464
2478
  void Promise.all(
2465
2479
  runnable.map(async (plan) => {
2466
2480
  try {
2481
+ await afterDebounce(plan);
2482
+ if (cancelled) return;
2467
2483
  const response = await hostRef.current.fetch(plan.endpoint, {
2468
2484
  method: "GET",
2469
2485
  signal: controller.signal
@@ -2496,6 +2512,7 @@ function SduiRenderer({
2496
2512
  return () => {
2497
2513
  cancelled = true;
2498
2514
  controller.abort();
2515
+ timers.forEach(clearTimeout);
2499
2516
  };
2500
2517
  }, [dataPlanKey, dataEpochKey, setEntries]);
2501
2518
  const dispatchRef = useRef(() => {
package/dist/meta.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as SduiPropValue, h as SduiNode } from './types-Dl4rPyan.js';
1
+ import { a as SduiPropValue, h as SduiNode } from './types-DO8_hC6A.js';
2
2
 
3
3
  /**
4
4
  * 시각 빌더용 컴포넌트 메타데이터 스키마.
package/dist/meta.js CHANGED
@@ -3308,6 +3308,15 @@ var JsonFieldMeta = {
3308
3308
  { name: "rows", kind: "number", label: "\uC904 \uC218", default: 6, group: "appearance" },
3309
3309
  { name: "placeholder", kind: "string", label: "\uC548\uB0B4 \uBB38\uAD6C", default: "JSON\uC744 \uC785\uB825\uD558\uC138\uC694", group: "content" },
3310
3310
  { name: "invalid", kind: "boolean", label: "\uC624\uB958 \uD45C\uC2DC", default: false, bindable: true, group: "validation" },
3311
+ {
3312
+ name: "emptyAsNull",
3313
+ kind: "boolean",
3314
+ label: "\uBE44\uC6B0\uBA74 null \uC800\uC7A5",
3315
+ default: false,
3316
+ description: "\uBE48 \uD14D\uC2A4\uD2B8\uB85C blur \uD558\uBA74 onChange(null) \u2014 \uC9C0\uC6CC\uC11C \uAC12\uC744 \uC0AD\uC81C\uD558\uB294 \uD3FC\uC5D0\uC11C \uCF20\uB2E4.",
3317
+ group: "behavior",
3318
+ advanced: true
3319
+ },
3311
3320
  { name: "disabled", kind: "boolean", label: "\uBE44\uD65C\uC131", default: false, bindable: true, group: "behavior" }
3312
3321
  ],
3313
3322
  children: { accepts: false },
@@ -216,6 +216,12 @@ interface SduiPageQuery {
216
216
  when?: SduiPropValue;
217
217
  /** 실패했거나 아직 조회 전일 때 상태에 들어갈 값. 생략하면 `undefined`. */
218
218
  fallback?: SduiPropValue;
219
+ /**
220
+ * 경로가 바뀐 뒤 이 시간(ms)만큼 조용해야 조회를 낸다. 경로가 입력값을 참조하는
221
+ * 조회(관리번호 실시간 검증 등)가 키 입력마다 GET 을 내는 것을 막는 자리다 —
222
+ * v2 는 400ms 를 썼다. 생략하면 즉시.
223
+ */
224
+ debounceMs?: number;
219
225
  }
220
226
  interface SduiPage {
221
227
  version: 1;
@@ -1,4 +1,4 @@
1
- export { AutocompleteField, toAutocompleteOptions } from '../chunk-TBAIREB3.js';
2
- import '../chunk-NIRR6KSQ.js';
1
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-3YFDIRCI.js';
2
+ import '../chunk-BJDWMECO.js';
3
3
  import '../chunk-B5JTXYDB.js';
4
4
  import '../chunk-SAN4IX6N.js';
@@ -14,6 +14,13 @@ interface JsonFieldProps {
14
14
  /** 객체 또는 JSON 문자열 */
15
15
  value?: unknown;
16
16
  onChange?: (parsed: unknown) => void;
17
+ /**
18
+ * 빈 텍스트로 blur 하면 onChange(null) 을 낸다 — "지워서 값 삭제"용.
19
+ * 기본은 종전대로 무동작이다: 빈 칸이 곧 null 저장이 되면, 잠깐 지우고 다시 쓰려던
20
+ * 사용자의 미완성 상태가 그대로 저장 후보가 되기 때문에 옵트인으로 둔다.
21
+ * (ERP 유닛 수정의 '옵션 비우기 (null 저장)' 버튼이 이 옵션으로 대체된다.)
22
+ */
23
+ emptyAsNull?: boolean;
17
24
  /** 기본 6 */
18
25
  rows?: number;
19
26
  invalid?: boolean;
@@ -1,3 +1,3 @@
1
- export { JsonField } from '../chunk-JONNWN6N.js';
1
+ export { JsonField } from '../chunk-FDVI3PWQ.js';
2
2
  import '../chunk-B5JTXYDB.js';
3
3
  import '../chunk-SAN4IX6N.js';
@@ -1,4 +1,4 @@
1
- export { SearchableMultiPicker } from '../chunk-OINKP2BE.js';
2
- import '../chunk-NIRR6KSQ.js';
1
+ export { SearchableMultiPicker } from '../chunk-IEUPUFRF.js';
2
+ import '../chunk-BJDWMECO.js';
3
3
  import '../chunk-B5JTXYDB.js';
4
4
  import '../chunk-SAN4IX6N.js';
@@ -1,5 +1,5 @@
1
- import { SearchableMultiPicker } from '../chunk-OINKP2BE.js';
2
- export { SearchableMultiPicker } from '../chunk-OINKP2BE.js';
1
+ import { SearchableMultiPicker } from '../chunk-IEUPUFRF.js';
2
+ export { SearchableMultiPicker } from '../chunk-IEUPUFRF.js';
3
3
  import { SecureScreen } from '../chunk-4CZJF5PR.js';
4
4
  export { SecureScreen } from '../chunk-4CZJF5PR.js';
5
5
  import { StackedMeter } from '../chunk-BMZKZGJF.js';
@@ -22,8 +22,8 @@ import { ImageLightbox } from '../chunk-3PYMVHL3.js';
22
22
  export { ImageLightbox } from '../chunk-3PYMVHL3.js';
23
23
  import { InlineEdit } from '../chunk-7AHNAX6T.js';
24
24
  export { InlineEdit } from '../chunk-7AHNAX6T.js';
25
- import { JsonField } from '../chunk-JONNWN6N.js';
26
- export { JsonField } from '../chunk-JONNWN6N.js';
25
+ import { JsonField } from '../chunk-FDVI3PWQ.js';
26
+ export { JsonField } from '../chunk-FDVI3PWQ.js';
27
27
  import { LiveClock } from '../chunk-Z3WGIFJP.js';
28
28
  export { LiveClock } from '../chunk-Z3WGIFJP.js';
29
29
  import { OrgChart } from '../chunk-BT4HC3WR.js';
@@ -32,8 +32,8 @@ import { PeriodFilter } from '../chunk-WJEGCCPY.js';
32
32
  export { PeriodFilter } from '../chunk-WJEGCCPY.js';
33
33
  import { PrintSheet } from '../chunk-JWYBFODH.js';
34
34
  export { PrintSheet } from '../chunk-JWYBFODH.js';
35
- export { AutocompleteField, toAutocompleteOptions } from '../chunk-TBAIREB3.js';
36
- import '../chunk-NIRR6KSQ.js';
35
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-3YFDIRCI.js';
36
+ import '../chunk-BJDWMECO.js';
37
37
  import { ColumnBoard } from '../chunk-VK6KWB65.js';
38
38
  export { ColumnBoard } from '../chunk-VK6KWB65.js';
39
39
  import { DiffView } from '../chunk-4IRLQWQC.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",