@wipco/sdui 0.8.1 → 0.9.1

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/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-D5Tkrynd.js';
4
- export { d as SduiCalcRef, e as SduiChild, f as SduiIndexRef, g as SduiItemRef, h as SduiNode, i as SduiScalar, j as SduiStateRef } from './types-D5Tkrynd.js';
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';
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';
package/dist/index.js CHANGED
@@ -1766,6 +1766,7 @@ function validateSpec(page, knownComponents) {
1766
1766
  const issues = [];
1767
1767
  if (!isPlainObject(page)) return [{ path: "$", message: "\uC2A4\uD399\uC774 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4" }];
1768
1768
  if (page.version !== 1) issues.push({ path: "$.version", message: "version\uC740 1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" });
1769
+ validatePageData(page.data, issues);
1769
1770
  if (!Array.isArray(page.body)) {
1770
1771
  issues.push({ path: "$.body", message: "body\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" });
1771
1772
  return issues;
@@ -1775,6 +1776,48 @@ function validateSpec(page, knownComponents) {
1775
1776
  );
1776
1777
  return issues;
1777
1778
  }
1779
+ function validatePageData(data, issues) {
1780
+ if (data === void 0) return;
1781
+ if (!isPlainObject(data)) {
1782
+ issues.push({ path: "$.data", message: "data\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1783
+ return;
1784
+ }
1785
+ for (const [key, raw] of Object.entries(data)) {
1786
+ const path = `$.data.${key}`;
1787
+ if (key === "" || key.startsWith("$")) {
1788
+ issues.push({ path, message: "\uC0C1\uD0DC \uD0A4\uB294 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC544\uC57C \uD558\uACE0 $\uB85C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" });
1789
+ continue;
1790
+ }
1791
+ if (!isPlainObject(raw)) {
1792
+ issues.push({ path, message: "\uC870\uD68C\uB294 { endpoint, select?, when?, fallback? } \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1793
+ continue;
1794
+ }
1795
+ if (raw.endpoint === void 0) {
1796
+ issues.push({ path: `${path}.endpoint`, message: "endpoint\uB294 \uD544\uC218\uC785\uB2C8\uB2E4" });
1797
+ } else if (typeof raw.endpoint === "string") {
1798
+ if (isDangerousUrlValue(raw.endpoint)) {
1799
+ issues.push({ path: `${path}.endpoint`, message: `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uACBD\uB85C: ${raw.endpoint}` });
1800
+ } else if (!raw.endpoint.startsWith("/")) {
1801
+ issues.push({ path: `${path}.endpoint`, message: "endpoint\uB294 /\uB85C \uC2DC\uC791\uD558\uB294 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1802
+ }
1803
+ } else if (!isPlainObject(raw.endpoint)) {
1804
+ issues.push({ path: `${path}.endpoint`, message: "endpoint\uB294 \uBB38\uC790\uC5F4\uC774\uAC70\uB098 \uAC12 \uCC38\uC870\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1805
+ } else {
1806
+ walkValue(raw.endpoint, `${path}.endpoint`, issues, false);
1807
+ }
1808
+ if (raw.select !== void 0 && typeof raw.select !== "string") {
1809
+ issues.push({ path: `${path}.select`, message: "select\uB294 \uBB38\uC790\uC5F4 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1810
+ }
1811
+ for (const [k, v] of Object.entries(raw)) {
1812
+ if (k === "endpoint" || k === "select") continue;
1813
+ if (k !== "when" && k !== "fallback") {
1814
+ issues.push({ path: `${path}.${k}`, message: `\uC54C \uC218 \uC5C6\uB294 \uD56D\uBAA9: ${k}` });
1815
+ continue;
1816
+ }
1817
+ walkValue(v, `${path}.${k}`, issues, false);
1818
+ }
1819
+ }
1820
+ }
1778
1821
  function walkChild(child, path, issues, known) {
1779
1822
  if (typeof child === "string") return;
1780
1823
  if (!isPlainObject(child)) {
@@ -1892,29 +1935,84 @@ function walkValue(value, path, issues, actionEligible) {
1892
1935
  walkValue(v, `${path}.${k}`, issues, k === "run" || k === "rowClick" || k.startsWith("on"));
1893
1936
  }
1894
1937
  }
1895
- function isPlainObject2(v) {
1938
+
1939
+ // src/page-data.ts
1940
+ function isPlainObject2(value) {
1941
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1942
+ }
1943
+ function planPageData(data, resolve) {
1944
+ if (data === void 0) return [];
1945
+ if (!isPlainObject2(data)) {
1946
+ warnOnce("page-data:root", "[sdui] page.data\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4 \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4");
1947
+ return [];
1948
+ }
1949
+ const plans = [];
1950
+ for (const [key, raw] of Object.entries(data)) {
1951
+ if (key === "" || key.startsWith("$")) {
1952
+ warnOnce(`page-data:${key}`, `[sdui] page.data\uC758 \uC0C1\uD0DC \uD0A4 "${key}"\uB294 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uAC74\uB108\uB701\uB2C8\uB2E4`);
1953
+ continue;
1954
+ }
1955
+ if (!isPlainObject2(raw)) {
1956
+ warnOnce(`page-data:${key}`, `[sdui] page.data.${key}\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4 \u2014 \uAC74\uB108\uB701\uB2C8\uB2E4`);
1957
+ continue;
1958
+ }
1959
+ const query = raw;
1960
+ const resolved = resolve(query.endpoint);
1961
+ const endpoint = typeof resolved === "string" ? resolved.trim() : "";
1962
+ const enabled = "when" in query ? Boolean(resolve(query.when)) : true;
1963
+ plans.push({
1964
+ key,
1965
+ endpoint,
1966
+ enabled,
1967
+ select: typeof query.select === "string" ? query.select : void 0,
1968
+ fallback: query.fallback
1969
+ });
1970
+ }
1971
+ return plans;
1972
+ }
1973
+ function planSignature(plans) {
1974
+ return JSON.stringify(plans.map((plan) => [plan.key, plan.enabled ? plan.endpoint : ""]));
1975
+ }
1976
+ function runnablePlans(plans) {
1977
+ return plans.filter((plan) => plan.enabled && plan.endpoint !== "");
1978
+ }
1979
+ function invalidateHits(plans, prefix, exact) {
1980
+ return plans.filter(
1981
+ (plan) => prefix === "" ? true : exact ? plan.endpoint === prefix : plan.endpoint.startsWith(prefix)
1982
+ ).map((plan) => plan.key);
1983
+ }
1984
+ function pickPath(source, path) {
1985
+ if (path === void 0 || path === "") return source;
1986
+ let value = source;
1987
+ for (const segment of path.split(".")) {
1988
+ if (!isPlainObject2(value)) return void 0;
1989
+ value = value[segment];
1990
+ }
1991
+ return value;
1992
+ }
1993
+ function isPlainObject3(v) {
1896
1994
  return typeof v === "object" && v !== null && !Array.isArray(v);
1897
1995
  }
1898
1996
  function isStateRef(v) {
1899
- return isPlainObject2(v) && typeof v.$state === "string";
1997
+ return isPlainObject3(v) && typeof v.$state === "string";
1900
1998
  }
1901
1999
  function isItemRef(v) {
1902
- return isPlainObject2(v) && "$item" in v;
2000
+ return isPlainObject3(v) && "$item" in v;
1903
2001
  }
1904
2002
  function isIndexRef(v) {
1905
- return isPlainObject2(v) && "$index" in v;
2003
+ return isPlainObject3(v) && "$index" in v;
1906
2004
  }
1907
2005
  function isCalcRef(v) {
1908
- return isPlainObject2(v) && isPlainObject2(v.$calc);
2006
+ return isPlainObject3(v) && isPlainObject3(v.$calc);
1909
2007
  }
1910
2008
  function isAction(v) {
1911
- return isPlainObject2(v) && typeof v.action === "string";
2009
+ return isPlainObject3(v) && typeof v.action === "string";
1912
2010
  }
1913
2011
  function isActionList(v) {
1914
2012
  return Array.isArray(v) && v.length > 0 && v.every(isAction);
1915
2013
  }
1916
2014
  function eventValue(arg) {
1917
- if (isPlainObject2(arg) && "target" in arg && typeof arg.preventDefault === "function") {
2015
+ if (isPlainObject3(arg) && "target" in arg && typeof arg.preventDefault === "function") {
1918
2016
  const target = arg.target;
1919
2017
  if (target.type === "checkbox") return target.checked;
1920
2018
  if (target.type === "number" || target.type === "range") {
@@ -2032,7 +2130,7 @@ function childrenOf(node) {
2032
2130
  }
2033
2131
  function renderChild(child, index, scope, engine) {
2034
2132
  if (typeof child === "string") return child;
2035
- if (!isPlainObject2(child)) {
2133
+ if (!isPlainObject3(child)) {
2036
2134
  warnOnce2(`child:${String(child)}`, `[sdui] \uC798\uBABB\uB41C \uB178\uB4DC(\uBB38\uC790\uC5F4 \uB610\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4): ${String(child)}`);
2037
2135
  return /* @__PURE__ */ jsx(SpecErrorCard, { children: "[sdui] \uC798\uBABB\uB41C \uB178\uB4DC \u2014 \uBB38\uC790\uC5F4 \uB610\uB294 \uB178\uB4DC \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" }, index);
2038
2136
  }
@@ -2047,11 +2145,11 @@ function SduiNodeRenderer({
2047
2145
  engine
2048
2146
  }) {
2049
2147
  const { registry, resolveValue, dispatch, resolveAsset } = engine;
2050
- const slots = isPlainObject2(node.slots) ? node.slots : void 0;
2148
+ const slots = isPlainObject3(node.slots) ? node.slots : void 0;
2051
2149
  const childList = childrenOf(node);
2052
2150
  if (node.component === "ForEach") {
2053
2151
  const source = resolveValue(
2054
- isPlainObject2(node.props) ? node.props.source ?? null : null,
2152
+ isPlainObject3(node.props) ? node.props.source ?? null : null,
2055
2153
  scope
2056
2154
  );
2057
2155
  const rows = Array.isArray(source) ? source : [];
@@ -2061,7 +2159,7 @@ function SduiNodeRenderer({
2061
2159
  }
2062
2160
  return /* @__PURE__ */ jsx(Fragment, { children: rows.map((item, rowIndex) => {
2063
2161
  const rowScope = [...scope, { item, index: rowIndex }];
2064
- const rowKey = isPlainObject2(item) && typeof item.id === "string" ? item.id : rowIndex;
2162
+ const rowKey = isPlainObject3(item) && typeof item.id === "string" ? item.id : rowIndex;
2065
2163
  return /* @__PURE__ */ jsx(React.Fragment, { children: childList?.map((child, i) => renderChild(child, i, rowScope, engine)) }, rowKey);
2066
2164
  }) });
2067
2165
  }
@@ -2077,7 +2175,7 @@ function SduiNodeRenderer({
2077
2175
  ] });
2078
2176
  }
2079
2177
  const props = {};
2080
- for (const [name, raw] of Object.entries(isPlainObject2(node.props) ? node.props : {})) {
2178
+ for (const [name, raw] of Object.entries(isPlainObject3(node.props) ? node.props : {})) {
2081
2179
  if (DENIED_PROPS2.has(name)) {
2082
2180
  warnOnce2(`prop:${name}`, `[sdui] \uC2A4\uD399\uC5D0\uC11C \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD504\uB86D: ${name} \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2083
2181
  continue;
@@ -2087,6 +2185,8 @@ function SduiNodeRenderer({
2087
2185
  props[name] = (arg) => actions.forEach(
2088
2186
  (a) => dispatch(resolveValue(a, scope), arg)
2089
2187
  );
2188
+ } else if (name === "rowClick" && (isAction(raw) || isActionList(raw))) {
2189
+ props[name] = raw;
2090
2190
  } else {
2091
2191
  let resolved = resolveValue(raw, scope);
2092
2192
  if (resolved === null) {
@@ -2109,7 +2209,7 @@ function SduiNodeRenderer({
2109
2209
  if (childLayout) {
2110
2210
  for (const name of CHILD_LAYOUT_PROPS) delete props[name];
2111
2211
  if (childLayout.style) {
2112
- props.style = isPlainObject2(props.style) ? { ...childLayout.style, ...props.style } : childLayout.style;
2212
+ props.style = isPlainObject3(props.style) ? { ...childLayout.style, ...props.style } : childLayout.style;
2113
2213
  }
2114
2214
  if (childLayout.className) {
2115
2215
  props.className = typeof props.className === "string" && props.className ? `${childLayout.className} ${props.className}` : childLayout.className;
@@ -2244,7 +2344,7 @@ function SduiRenderer({
2244
2344
  if (head in current) {
2245
2345
  let value = current[head];
2246
2346
  for (const segment of segments.slice(cut)) {
2247
- if (!isPlainObject2(value)) return void 0;
2347
+ if (!isPlainObject3(value)) return void 0;
2248
2348
  value = value[segment];
2249
2349
  }
2250
2350
  return value;
@@ -2262,7 +2362,7 @@ function SduiRenderer({
2262
2362
  const frame = scope[scope.length - 1];
2263
2363
  if (!frame) return null;
2264
2364
  if (value.$item === true) return frame.item;
2265
- return isPlainObject2(frame.item) ? frame.item[value.$item] ?? null : null;
2365
+ return isPlainObject3(frame.item) ? frame.item[value.$item] ?? null : null;
2266
2366
  }
2267
2367
  if (isIndexRef(value)) {
2268
2368
  return scope[scope.length - 1]?.index ?? null;
@@ -2278,7 +2378,7 @@ function SduiRenderer({
2278
2378
  const resolved = args.map((arg) => resolveValue(arg, scope));
2279
2379
  const nums = (v) => {
2280
2380
  if (Array.isArray(v))
2281
- return v.map((row) => field && isPlainObject2(row) ? toNumber2(row[field]) : toNumber2(row));
2381
+ return v.map((row) => field && isPlainObject3(row) ? toNumber2(row[field]) : toNumber2(row));
2282
2382
  return [toNumber2(v)];
2283
2383
  };
2284
2384
  switch (op) {
@@ -2325,7 +2425,7 @@ function SduiRenderer({
2325
2425
  }
2326
2426
  }
2327
2427
  if (Array.isArray(value)) return value.map((v) => resolveValue(v, scope));
2328
- if (isPlainObject2(value)) {
2428
+ if (isPlainObject3(value)) {
2329
2429
  const out = {};
2330
2430
  for (const [k, v] of Object.entries(value)) out[k] = resolveValue(v, scope);
2331
2431
  return out;
@@ -2334,10 +2434,78 @@ function SduiRenderer({
2334
2434
  },
2335
2435
  [lookupState]
2336
2436
  );
2437
+ const dataPlan = useMemo(
2438
+ () => planPageData(page.data, (value) => resolveValue(value, EMPTY_SCOPE)),
2439
+ // state를 의존성에 두어야 $state를 참조하는 경로가 값 변화를 따라간다.
2440
+ [page.data, resolveValue, state]
2441
+ );
2442
+ const dataPlanRef = useRef(dataPlan);
2443
+ dataPlanRef.current = dataPlan;
2444
+ const dataPlanKey = useMemo(() => planSignature(dataPlan), [dataPlan]);
2445
+ const [dataEpochs, setDataEpochs] = useState({});
2446
+ const dataEpochsRef = useRef(dataEpochs);
2447
+ dataEpochsRef.current = dataEpochs;
2448
+ const fetchedRef = useRef(/* @__PURE__ */ new Map());
2449
+ const dataEpochKey = useMemo(() => JSON.stringify(dataEpochs), [dataEpochs]);
2450
+ useEffect(() => {
2451
+ const epochs = dataEpochsRef.current;
2452
+ const runnable = runnablePlans(dataPlanRef.current).filter((plan) => {
2453
+ const stamp = `${plan.endpoint}#${epochs[plan.key] ?? 0}`;
2454
+ if (fetchedRef.current.get(plan.key) === stamp) return false;
2455
+ fetchedRef.current.set(plan.key, stamp);
2456
+ return true;
2457
+ });
2458
+ if (runnable.length === 0) return;
2459
+ const controller = new AbortController();
2460
+ let cancelled = false;
2461
+ setEntries(
2462
+ Object.fromEntries(runnable.map((plan) => [`${plan.key}.$loading`, true]))
2463
+ );
2464
+ void Promise.all(
2465
+ runnable.map(async (plan) => {
2466
+ try {
2467
+ const response = await hostRef.current.fetch(plan.endpoint, {
2468
+ method: "GET",
2469
+ signal: controller.signal
2470
+ });
2471
+ if (cancelled) return;
2472
+ const picked = pickPath(response, plan.select);
2473
+ if (picked === void 0 && plan.select !== void 0) {
2474
+ warnOnce2(
2475
+ `data-select:${plan.key}`,
2476
+ `[sdui] page.data.${plan.key}: \uC751\uB2F5\uC5D0 "${plan.select}" \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uAC12 \uC5C6\uC74C\uC73C\uB85C \uB461\uB2C8\uB2E4 (${plan.endpoint})`
2477
+ );
2478
+ }
2479
+ setEntries({
2480
+ [plan.key]: picked,
2481
+ [`${plan.key}.$loading`]: false,
2482
+ [`${plan.key}.$error`]: null
2483
+ });
2484
+ } catch (error) {
2485
+ if (cancelled || controller.signal.aborted) return;
2486
+ const message = error instanceof Error ? error.message : "\uC870\uD68C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
2487
+ warnOnce2(`data:${plan.key}:${plan.endpoint}`, `[sdui] page.data.${plan.key} \uC870\uD68C \uC2E4\uD328 (${plan.endpoint}): ${message}`);
2488
+ setEntries({
2489
+ [plan.key]: plan.fallback ?? void 0,
2490
+ [`${plan.key}.$loading`]: false,
2491
+ [`${plan.key}.$error`]: message
2492
+ });
2493
+ }
2494
+ })
2495
+ );
2496
+ return () => {
2497
+ cancelled = true;
2498
+ controller.abort();
2499
+ };
2500
+ }, [dataPlanKey, dataEpochKey, setEntries]);
2337
2501
  const dispatchRef = useRef(() => {
2338
2502
  });
2339
2503
  const dispatch = useCallback(
2340
- (action, arg) => {
2504
+ (rawAction, arg) => {
2505
+ const action = resolveValue(
2506
+ rawAction,
2507
+ EMPTY_SCOPE
2508
+ );
2341
2509
  switch (action.action) {
2342
2510
  case "navigate":
2343
2511
  if (!isSafeNavigationTarget(action.to)) {
@@ -2407,7 +2575,7 @@ function SduiRenderer({
2407
2575
  break;
2408
2576
  }
2409
2577
  const row = current[index];
2410
- if (!isPlainObject2(row)) break;
2578
+ if (!isPlainObject3(row)) break;
2411
2579
  const nextValue = (action.fromEvent ? eventValue(arg) : action.value) ?? null;
2412
2580
  const next = [...current];
2413
2581
  next[index] = { ...row, [action.field]: nextValue };
@@ -2456,9 +2624,19 @@ function SduiRenderer({
2456
2624
  })();
2457
2625
  break;
2458
2626
  }
2459
- case "invalidate":
2627
+ case "invalidate": {
2460
2628
  hostRef.current.invalidate?.(action.prefix, action.exact ?? false);
2629
+ const prefix = typeof action.prefix === "string" ? action.prefix : "";
2630
+ const hit = invalidateHits(runnablePlans(dataPlanRef.current), prefix, action.exact ?? false);
2631
+ if (hit.length > 0) {
2632
+ setDataEpochs((prev) => {
2633
+ const next = { ...prev };
2634
+ for (const key of hit) next[key] = (next[key] ?? 0) + 1;
2635
+ return next;
2636
+ });
2637
+ }
2461
2638
  break;
2639
+ }
2462
2640
  case "refresh":
2463
2641
  hostRef.current.refresh?.();
2464
2642
  break;
@@ -2496,7 +2674,7 @@ function SduiRenderer({
2496
2674
  break;
2497
2675
  }
2498
2676
  },
2499
- [pushToast, onCustomAction, customValidators, setEntries]
2677
+ [pushToast, onCustomAction, customValidators, setEntries, resolveValue]
2500
2678
  );
2501
2679
  dispatchRef.current = dispatch;
2502
2680
  const engine = useMemo(
package/dist/meta.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as SduiPropValue, h as SduiNode } from './types-D5Tkrynd.js';
1
+ import { a as SduiPropValue, h as SduiNode } from './types-Dl4rPyan.js';
2
2
 
3
3
  /**
4
4
  * 시각 빌더용 컴포넌트 메타데이터 스키마.
@@ -182,6 +182,41 @@ type SduiAction = {
182
182
  name: string;
183
183
  payload?: SduiPropValue;
184
184
  };
185
+ /**
186
+ * 페이지가 열릴 때 서버에서 끌어와 상태에 넣을 조회 하나.
187
+ *
188
+ * ## 왜 이게 생겼나 (실측)
189
+ * 이것이 생기기 전, 스펙에는 **서버 데이터를 상태로 들여오는 통로가 없었다.**
190
+ * 마운트 훅이 없고, `submitForm`은 GET에도 본문을 붙여(어댑터가 `JSON.stringify`한다)
191
+ * 브라우저가 막는다. 그래서 데이터는 **스스로 조회하는 컴포넌트**로만 들어왔다 —
192
+ * ERP 85화면 실측에서 그런 자리는 `RemoteSearchResults +endpoint` 43곳,
193
+ * `AutocompleteField +endpoint` 1곳, 합쳐 44곳이 전부였다.
194
+ * 그 결과 `Chart`는 20번 쓰여 **20번 모두** 리터럴 샘플이었고, 데이터를 지는 자리
195
+ * 854곳 중 387곳이 샘플로 남아 있었다. 화면마다 배선을 빠뜨린 것이 아니라
196
+ * **배선할 수단이 없었던 것**이다.
197
+ */
198
+ interface SduiPageQuery {
199
+ /**
200
+ * GET 경로. 값 해석을 거치므로 `$state`·`$calc`를 쓸 수 있다 —
201
+ * 선택된 행에 딸린 조회는
202
+ * `{"$calc":{"op":"concat","args":["/roles/",{"$state":"rt.id"},"/permissions"]}}`처럼 쓴다.
203
+ * 해석된 경로가 바뀌면 자동으로 다시 조회한다.
204
+ */
205
+ endpoint: SduiPropValue;
206
+ /**
207
+ * 응답에서 꺼낼 필드 경로(`"data"`, `"result.rows"`). 생략하면 응답 전체.
208
+ * 목록 API가 `{data, total, limit, offset}` 봉투를 주므로 대개 `"data"`가 된다.
209
+ */
210
+ select?: string;
211
+ /**
212
+ * 이 값이 거짓이면 조회하지 않는다. 선택된 행이 없을 때
213
+ * `/roles/undefined/permissions`를 때리는 것을 막는 자리다.
214
+ * 생략하면 해석된 경로가 비어 있지 않은 문자열일 때 조회한다.
215
+ */
216
+ when?: SduiPropValue;
217
+ /** 실패했거나 아직 조회 전일 때 상태에 들어갈 값. 생략하면 `undefined`. */
218
+ fallback?: SduiPropValue;
219
+ }
185
220
  interface SduiPage {
186
221
  version: 1;
187
222
  /** 문서 <title> 및 페이지 헤더에 쓰인다. */
@@ -191,7 +226,19 @@ interface SduiPage {
191
226
  subscribe?: string[];
192
227
  /** URL 쿼리파라미터와 동기화할 상태 키 — 딥링크/새로고침 보존용 */
193
228
  urlState?: string[];
229
+ /**
230
+ * 페이지가 열릴 때 채울 상태 — `{ "상태키": { endpoint, select? } }`.
231
+ *
232
+ * 각 조회는 자기 키 옆에 진행·실패를 **평평한 키**로 함께 적는다:
233
+ * `"<키>.$loading"`(boolean) · `"<키>.$error"`(문자열 또는 null).
234
+ * 상태 조회가 완전 일치를 먼저 보므로 `{"$state":"rt.x.$loading"}`은 그 평평한 키를
235
+ * 집고 `{"$state":"rt.x"}`는 데이터를 집는다 — 서로 가리지 않는다.
236
+ *
237
+ * `invalidate` 액션의 prefix와 경로가 겹치면 다시 조회한다. 저장 후
238
+ * `{"action":"invalidate","prefix":"/roles"}` 하나로 목록이 갱신되는 이유다.
239
+ */
240
+ data?: Record<string, SduiPageQuery>;
194
241
  body: SduiChild[];
195
242
  }
196
243
 
197
- export type { SduiAction as S, SduiPropValue as a, SduiRule as b, SduiPage as c, SduiCalcRef as d, SduiChild as e, SduiIndexRef as f, SduiItemRef as g, SduiNode as h, SduiScalar as i, SduiStateRef as j };
244
+ export type { SduiAction as S, SduiPropValue as a, SduiRule as b, SduiPage as c, SduiCalcRef as d, SduiChild as e, SduiIndexRef as f, SduiItemRef as g, SduiNode as h, SduiPageQuery as i, SduiScalar as j, SduiStateRef as k };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",