@wipco/sdui 0.30.0 → 0.31.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,67 @@
1
+ import { Spinner, EmptyState } from './chunk-WIDDHR7L.js';
2
+ import { useState, useEffect } from 'react';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ function useOptionalModule(load) {
6
+ const [state, setState] = useState({ status: "loading" });
7
+ useEffect(() => {
8
+ let alive = true;
9
+ load().then(
10
+ (module) => {
11
+ if (alive) setState({ status: "ready", module });
12
+ },
13
+ (error) => {
14
+ if (!alive) return;
15
+ console.warn("[sdui] \uC120\uD0DD \uC758\uC874\uC131 \uB85C\uB4DC \uC2E4\uD328 \u2014 \uD50C\uB808\uC774\uC2A4\uD640\uB354\uB85C \uB300\uCCB4\uD569\uB2C8\uB2E4", error);
16
+ setState({ status: "missing" });
17
+ }
18
+ );
19
+ return () => {
20
+ alive = false;
21
+ };
22
+ }, [load]);
23
+ return state;
24
+ }
25
+ var optionalModuleCache = /* @__PURE__ */ new WeakMap();
26
+ function loadOptionalModule(load, packageName) {
27
+ const cached = optionalModuleCache.get(load);
28
+ if (cached) return cached;
29
+ const pending = load().catch((error) => {
30
+ console.warn(`[sdui] \uC120\uD0DD \uC758\uC874\uC131 '${packageName}' \uB85C\uB4DC \uC2E4\uD328`, error);
31
+ optionalModuleCache.delete(load);
32
+ throw new Error(`${packageName} \uD328\uD0A4\uC9C0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uC124\uCE58\uD574 \uC8FC\uC138\uC694`);
33
+ });
34
+ optionalModuleCache.set(load, pending);
35
+ return pending;
36
+ }
37
+ function DependencyPlaceholder({
38
+ packageName,
39
+ purpose,
40
+ height,
41
+ loading = false
42
+ }) {
43
+ return /* @__PURE__ */ jsx(
44
+ "div",
45
+ {
46
+ style: {
47
+ minHeight: height,
48
+ display: "flex",
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ border: "1px solid var(--wp-border-subtle)",
52
+ borderRadius: "var(--wp-radius-md)",
53
+ background: "var(--wp-surface-sunken)"
54
+ },
55
+ children: loading ? /* @__PURE__ */ jsx(Spinner, { label: `${purpose} \uC900\uBE44 \uC911` }) : /* @__PURE__ */ jsxs(EmptyState, { tight: true, icon: "box", title: `${purpose}\uB97C \uADF8\uB9AC\uB824\uBA74 ${packageName} \uD328\uD0A4\uC9C0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`, children: [
56
+ "\uC120\uD0DD \uC758\uC874\uC131\uC774\uB77C \uAE30\uBCF8 \uC124\uCE58\uC5D0\uB294 \uD3EC\uD568\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 ",
57
+ /* @__PURE__ */ jsxs("code", { children: [
58
+ "pnpm add ",
59
+ packageName
60
+ ] }),
61
+ " \uD6C4 \uB2E4\uC2DC \uC5F4\uC5B4 \uC8FC\uC138\uC694."
62
+ ] })
63
+ }
64
+ );
65
+ }
66
+
67
+ export { DependencyPlaceholder, loadOptionalModule, useOptionalModule };
@@ -1,5 +1,5 @@
1
1
  import { useDismiss, usePopoverAnchor, usePortalTo, HoistedStyle, AnchoredPopoverStyles, anchorStyle, focusStaysInside } from './chunk-EFMKWPZ3.js';
2
- import { Icon, Spinner, EmptyState } from './chunk-WIDDHR7L.js';
2
+ import { Icon } from './chunk-WIDDHR7L.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -194,66 +194,5 @@ var TP_CSS = `
194
194
  .wp-tp-opt--on{background:var(--wp-action-primary-bg);color:var(--wp-action-primary-fg);font-weight:500}
195
195
  `;
196
196
  var TimePicker = boxed(TimePickerImpl, "TimePicker");
197
- function useOptionalModule(load) {
198
- const [state, setState] = useState({ status: "loading" });
199
- useEffect(() => {
200
- let alive = true;
201
- load().then(
202
- (module) => {
203
- if (alive) setState({ status: "ready", module });
204
- },
205
- (error) => {
206
- if (!alive) return;
207
- console.warn("[sdui] \uC120\uD0DD \uC758\uC874\uC131 \uB85C\uB4DC \uC2E4\uD328 \u2014 \uD50C\uB808\uC774\uC2A4\uD640\uB354\uB85C \uB300\uCCB4\uD569\uB2C8\uB2E4", error);
208
- setState({ status: "missing" });
209
- }
210
- );
211
- return () => {
212
- alive = false;
213
- };
214
- }, [load]);
215
- return state;
216
- }
217
- var optionalModuleCache = /* @__PURE__ */ new WeakMap();
218
- function loadOptionalModule(load, packageName) {
219
- const cached = optionalModuleCache.get(load);
220
- if (cached) return cached;
221
- const pending = load().catch((error) => {
222
- console.warn(`[sdui] \uC120\uD0DD \uC758\uC874\uC131 '${packageName}' \uB85C\uB4DC \uC2E4\uD328`, error);
223
- optionalModuleCache.delete(load);
224
- throw new Error(`${packageName} \uD328\uD0A4\uC9C0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uC124\uCE58\uD574 \uC8FC\uC138\uC694`);
225
- });
226
- optionalModuleCache.set(load, pending);
227
- return pending;
228
- }
229
- function DependencyPlaceholder({
230
- packageName,
231
- purpose,
232
- height,
233
- loading = false
234
- }) {
235
- return /* @__PURE__ */ jsx(
236
- "div",
237
- {
238
- style: {
239
- minHeight: height,
240
- display: "flex",
241
- alignItems: "center",
242
- justifyContent: "center",
243
- border: "1px solid var(--wp-border-subtle)",
244
- borderRadius: "var(--wp-radius-md)",
245
- background: "var(--wp-surface-sunken)"
246
- },
247
- children: loading ? /* @__PURE__ */ jsx(Spinner, { label: `${purpose} \uC900\uBE44 \uC911` }) : /* @__PURE__ */ jsxs(EmptyState, { tight: true, icon: "box", title: `${purpose}\uB97C \uADF8\uB9AC\uB824\uBA74 ${packageName} \uD328\uD0A4\uC9C0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`, children: [
248
- "\uC120\uD0DD \uC758\uC874\uC131\uC774\uB77C \uAE30\uBCF8 \uC124\uCE58\uC5D0\uB294 \uD3EC\uD568\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 ",
249
- /* @__PURE__ */ jsxs("code", { children: [
250
- "pnpm add ",
251
- packageName
252
- ] }),
253
- " \uD6C4 \uB2E4\uC2DC \uC5F4\uC5B4 \uC8FC\uC138\uC694."
254
- ] })
255
- }
256
- );
257
- }
258
197
 
259
- export { DependencyPlaceholder, TimePicker, loadOptionalModule, useOptionalModule };
198
+ export { TimePicker };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { TimePicker } from './chunk-WXOU7QTN.js';
2
- export { TimePicker, loadOptionalModule } from './chunk-WXOU7QTN.js';
1
+ import { TimePicker } from './chunk-SNRKPEWT.js';
2
+ export { TimePicker } from './chunk-SNRKPEWT.js';
3
+ export { loadOptionalModule } from './chunk-RRMKCZYQ.js';
3
4
  import { useSduiDispatch, SduiDialog, useSduiCan, SduiDispatchContext, SduiPermissionProvider } from './chunk-QU7YVMFA.js';
4
5
  export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch } from './chunk-QU7YVMFA.js';
5
6
  import { ImageViewer } from './chunk-THEP3VWY.js';
package/dist/meta.d.ts CHANGED
@@ -431,6 +431,7 @@ declare const OrgChartBoardMeta: SduiComponentMeta;
431
431
  declare const PermissionMatrixEditorMeta: SduiComponentMeta;
432
432
  declare const RoleMemberPickerMeta: SduiComponentMeta;
433
433
  declare const FileDropzoneMeta: SduiComponentMeta;
434
+ declare const ExcelUploadCardMeta: SduiComponentMeta;
434
435
  declare const AutocompleteFieldMeta: SduiComponentMeta;
435
436
  declare const InlineEditMeta: SduiComponentMeta;
436
437
  declare const JsonFieldMeta: SduiComponentMeta;
@@ -456,7 +457,7 @@ declare const StripMeta: SduiComponentMeta;
456
457
  /**
457
458
  * 위젯 어휘의 메타데이터.
458
459
  *
459
- * 키 타입이 `SduiWidgetComponentName | 'AutocompleteField' | 'FileDropzone'`인 이유:
460
+ * 키 타입이 `SduiWidgetComponentName | 'AutocompleteField' | 'FileDropzone' | 'ExcelUploadCard'`인 이유:
460
461
  * 앞의 20종은 `widgetComponents`와 키가 어긋나면 컴파일 에러가 나야 하고(core·rich와 같다),
461
462
  * `AutocompleteField`는 그 묶음에 **일부러 빠져 있는** 이름이라 union에 직접 적어야 한다.
462
463
  * 표시 계층만 라이브러리에 있고 조회는 앱 어댑터가 얹는 구조인데(widgets/index.ts 참고),
@@ -467,6 +468,7 @@ declare const widgetComponentMeta: {
467
468
  ApprovalLineEditor: SduiComponentMeta;
468
469
  ApprovalOverridesEditor: SduiComponentMeta;
469
470
  ApprovalStepper: SduiComponentMeta;
471
+ ExcelUploadCard: SduiComponentMeta;
470
472
  FileDropzone: SduiComponentMeta;
471
473
  PermissionMatrixEditor: SduiComponentMeta;
472
474
  RoleMemberPicker: SduiComponentMeta;
@@ -604,4 +606,4 @@ declare function paletteGroups(catalog?: Record<string, SduiComponentMeta>): Arr
604
606
  items: SduiComponentMeta[];
605
607
  }>;
606
608
 
607
- export { ActionsMeta, AlertMeta, ApprovalLineEditorMeta, ApprovalOverridesEditorMeta, ApprovalStepperMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, FieldMeta, FileDropzoneMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartBoardMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PermissionMatrixEditorMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RankBoardMeta, RecordSheetMeta, RemoteSearchResultsMeta, RoleMemberPickerMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, type SduiCategory, type SduiChildrenMeta, type SduiComponentMeta, type SduiDomMeta, type SduiEnumOption, type SduiPropGroup, type SduiPropKind, type SduiPropMeta, type SduiResizeMeta, type SduiSlotMeta, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimePickerMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
609
+ export { ActionsMeta, AlertMeta, ApprovalLineEditorMeta, ApprovalOverridesEditorMeta, ApprovalStepperMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, ExcelUploadCardMeta, FieldMeta, FileDropzoneMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartBoardMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PermissionMatrixEditorMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RankBoardMeta, RecordSheetMeta, RemoteSearchResultsMeta, RoleMemberPickerMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, type SduiCategory, type SduiChildrenMeta, type SduiComponentMeta, type SduiDomMeta, type SduiEnumOption, type SduiPropGroup, type SduiPropKind, type SduiPropMeta, type SduiResizeMeta, type SduiSlotMeta, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimePickerMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
package/dist/meta.js CHANGED
@@ -3949,6 +3949,117 @@ var FileDropzoneMeta = {
3949
3949
  },
3950
3950
  resize: { kind: "none" }
3951
3951
  };
3952
+ var ExcelUploadCardMeta = {
3953
+ name: "ExcelUploadCard",
3954
+ label: "\uC5D1\uC140 \uC77C\uAD04 \uC5C5\uB85C\uB4DC",
3955
+ category: "data",
3956
+ icon: "cloud-upload",
3957
+ description: "\uC5D1\uC140 \uC77C\uAD04 \uC5C5\uB85C\uB4DC \uCE74\uB4DC. server\uB294 \uD30C\uC77C\uC744 \uADF8\uB300\uB85C \uC62C\uB824 \uCC98\uB9AC \uC694\uC57D\uC744 \uBC1B\uACE0, preview\uB294 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uD30C\uC2F1\xB7\uAC80\uC99D\uD558\uACE0 \uC624\uB958 \uC140\uC744 \uACE0\uCE5C \uB4A4 \uC720\uD6A8 \uD589\uB9CC \uBCF4\uB0B8\uB2E4.",
3958
+ props: [
3959
+ {
3960
+ name: "mode",
3961
+ kind: "enum",
3962
+ label: "\uB3D9\uC791 \uBC29\uC2DD",
3963
+ default: "server",
3964
+ options: [
3965
+ { value: "server", label: "\uC11C\uBC84 \uC5C5\uB85C\uB4DC", description: "\uD30C\uC77C\uC744 multipart\uB85C \uC62C\uB9AC\uACE0 \uCC98\uB9AC \uC694\uC57D\xB7\uD589 \uC624\uB958\uB97C \uBC1B\uB294\uB2E4" },
3966
+ { value: "preview", label: "\uBBF8\uB9AC\uBCF4\uAE30 \uAC80\uC99D", description: "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uD30C\uC2F1\xB7\uAC80\uC99D\uD558\uACE0 \uC624\uB958 \uC140\uC744 \uC778\uB77C\uC778 \uC218\uC815\uD55C \uB4A4 \uC720\uD6A8 \uD589\uB9CC \uBCF4\uB0B8\uB2E4" }
3967
+ ],
3968
+ group: "behavior"
3969
+ },
3970
+ // 실측 8/8 (preview×5, server×3)
3971
+ {
3972
+ name: "endpoint",
3973
+ kind: "string",
3974
+ label: "\uC5C5\uB85C\uB4DC \uACBD\uB85C",
3975
+ description: "server\uBA74 \uD30C\uC77C multipart POST \uB300\uC0C1, preview\uBA74 \uAC80\uC99D\uB41C { rows } JSON POST \uB300\uC0C1.",
3976
+ group: "data"
3977
+ },
3978
+ // 실측 7/8
3979
+ {
3980
+ name: "templateColumns",
3981
+ kind: "array",
3982
+ label: "\uD15C\uD50C\uB9BF \uCEEC\uB7FC",
3983
+ description: "\uC788\uC73C\uBA74 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uD15C\uD50C\uB9BF xlsx\uB97C \uB9CC\uB4E0\uB2E4(\uBC31\uC5D4\uB4DC \uBD88\uD544\uC694). 2\uD589\uC5D0 \uC608\uC2DC\uAC12\uC774 \uB4E4\uC5B4\uAC04\uB2E4.",
3984
+ group: "data",
3985
+ // 실측 8/8, 원소 54개 — key·header·example 세 키가 모두 54/54로 항상 채워져 있다.
3986
+ item: [
3987
+ { name: "key", kind: "string", label: "\uD0A4", required: true },
3988
+ { name: "header", kind: "string", label: "\uD5E4\uB354", required: true },
3989
+ { name: "example", kind: "string", label: "\uC608\uC2DC\uAC12", description: "\uD15C\uD50C\uB9BF 2\uD589\uC5D0 \uB4E4\uC5B4\uAC04\uB2E4." }
3990
+ ]
3991
+ },
3992
+ {
3993
+ name: "columns",
3994
+ kind: "array",
3995
+ label: "\uAC80\uC99D \uC2A4\uD0A4\uB9C8",
3996
+ description: "preview \uBAA8\uB4DC\uC758 \uD30C\uC2F1\xB7\uAC80\uC99D \uAE30\uC900. \uC2E4\uCE21\uC0C1 preview \uC778\uC2A4\uD134\uC2A4 5\uAC1C\uC640 \uC815\uD655\uD788 \uC77C\uCE58\uD55C\uB2E4.",
3997
+ group: "data",
3998
+ // 실측 5/8, 원소 34개 — key·header 34/34, required 17/34(값은 전부 true), options 9/34.
3999
+ item: [
4000
+ { name: "key", kind: "string", label: "\uD0A4", required: true },
4001
+ { name: "header", kind: "string", label: "\uC5D1\uC140 \uD5E4\uB354", required: true, description: "\uD55C\uAD6D\uC5B4 \uBCC4\uCE6D. key\uC640\uB3C4 \uB9E4\uCE6D\uB41C\uB2E4." },
4002
+ { name: "required", kind: "boolean", label: "\uD544\uC218", default: false },
4003
+ {
4004
+ name: "options",
4005
+ kind: "array",
4006
+ label: "\uD5C8\uC6A9\uAC12",
4007
+ description: "\uC788\uC73C\uBA74 \uC624\uB958 \uC140\uC744 Select\uB85C, \uC5C6\uC73C\uBA74 Input\uC73C\uB85C \uACE0\uCE5C\uB2E4.",
4008
+ item: [],
4009
+ itemKind: "string"
4010
+ }
4011
+ ]
4012
+ },
4013
+ {
4014
+ name: "summaryLabels",
4015
+ kind: "object",
4016
+ label: "\uC694\uC57D \uD0C0\uC77C \uB77C\uBCA8",
4017
+ // fields를 적지 않아 자유 JSON으로 둔다 — 키가 백엔드 응답의 숫자 필드 이름이라
4018
+ // 엔드포인트마다 다르다. 실측에서도 rows(3)·createdUnits(2) 외에 skippedRows·
4019
+ // createdAssets·skippedUnits·createdBorrows·skippedBorrows가 각 1회씩으로 전부 달랐다.
4020
+ description: "\uC11C\uBC84 \uC751\uB2F5\uC758 \uC22B\uC790 \uD544\uB4DC \uC774\uB984 \u2192 \uC694\uC57D \uD0C0\uC77C\uC5D0 \uC4F8 \uB77C\uBCA8. server \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC4F0\uC778\uB2E4.",
4021
+ group: "content"
4022
+ },
4023
+ // 실측 3/8 — server 인스턴스 3개와 일치
4024
+ {
4025
+ name: "templateEndpoint",
4026
+ kind: "string",
4027
+ label: "\uD15C\uD50C\uB9BF \uB2E4\uC6B4\uB85C\uB4DC \uACBD\uB85C",
4028
+ description: "templateColumns\uAC00 \uC5C6\uC744 \uB54C\uB9CC \uC4F0\uC778\uB2E4(blob \uB2E4\uC6B4\uB85C\uB4DC).",
4029
+ group: "data",
4030
+ advanced: true
4031
+ },
4032
+ // 실측 0/8 — 구현에는 있다
4033
+ {
4034
+ name: "onComplete",
4035
+ kind: "action",
4036
+ label: "\uC644\uB8CC",
4037
+ eventArg: "server\uBA74 \uC751\uB2F5 JSON, preview\uBA74 \uC11C\uBC84 \uC751\uB2F5 \uB610\uB294 \uAC80\uC99D\uB41C \uC720\uD6A8 \uD589 \uBC30\uC5F4",
4038
+ group: "behavior"
4039
+ }
4040
+ // 실측 5/8 — 단일 toast×3, 액션 배열×2(close→toast)
4041
+ ],
4042
+ children: { accepts: false },
4043
+ dom: { element: "div" },
4044
+ // 실측: 루트가 라이브러리 Card → <div class="wp-card">
4045
+ // 프리셋에 endpoint·templateEndpoint를 넣지 않는다. 이 조합이면 템플릿은 브라우저에서
4046
+ // xlsx로 만들고, 가져오기도 onComplete만 부른다 → 바깥으로 나가는 요청이 0건이다.
4047
+ preset: {
4048
+ component: "ExcelUploadCard",
4049
+ props: {
4050
+ mode: "preview",
4051
+ templateColumns: [
4052
+ { key: "name", header: "\uD488\uBAA9\uBA85", example: "\uB178\uD2B8\uBD81" },
4053
+ { key: "qty", header: "\uC218\uB7C9", example: "3" }
4054
+ ],
4055
+ columns: [
4056
+ { key: "name", header: "\uD488\uBAA9\uBA85", required: true },
4057
+ { key: "qty", header: "\uC218\uB7C9", required: true }
4058
+ ]
4059
+ }
4060
+ },
4061
+ resize: { kind: "none" }
4062
+ };
3952
4063
  var AutocompleteFieldMeta = {
3953
4064
  name: "AutocompleteField",
3954
4065
  label: "\uC790\uB3D9\uC644\uC131",
@@ -5369,6 +5480,7 @@ var widgetComponentMeta = {
5369
5480
  ApprovalLineEditor: ApprovalLineEditorMeta,
5370
5481
  ApprovalOverridesEditor: ApprovalOverridesEditorMeta,
5371
5482
  ApprovalStepper: ApprovalStepperMeta,
5483
+ ExcelUploadCard: ExcelUploadCardMeta,
5372
5484
  FileDropzone: FileDropzoneMeta,
5373
5485
  PermissionMatrixEditor: PermissionMatrixEditorMeta,
5374
5486
  RoleMemberPicker: RoleMemberPickerMeta,
@@ -5431,4 +5543,4 @@ function paletteGroups(catalog = allComponentMeta) {
5431
5543
  })).filter((group) => group.items.length > 0);
5432
5544
  }
5433
5545
 
5434
- export { ActionsMeta, AlertMeta, ApprovalLineEditorMeta, ApprovalOverridesEditorMeta, ApprovalStepperMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, FieldMeta, FileDropzoneMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartBoardMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PermissionMatrixEditorMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RankBoardMeta, RecordSheetMeta, RemoteSearchResultsMeta, RoleMemberPickerMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimePickerMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
5546
+ export { ActionsMeta, AlertMeta, ApprovalLineEditorMeta, ApprovalOverridesEditorMeta, ApprovalStepperMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, ExcelUploadCardMeta, FieldMeta, FileDropzoneMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartBoardMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PermissionMatrixEditorMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RankBoardMeta, RecordSheetMeta, RemoteSearchResultsMeta, RoleMemberPickerMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimePickerMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
@@ -0,0 +1,37 @@
1
+ import { S as SduiBoxProps } from '../layout-B1QrIBm6.js';
2
+ import React__default from 'react';
3
+
4
+ interface ExcelTemplateColumn {
5
+ key: string;
6
+ header: string;
7
+ /** 템플릿 2행에 들어갈 예시값 */
8
+ example?: string;
9
+ }
10
+ interface ExcelPreviewColumn {
11
+ key: string;
12
+ /** 엑셀 헤더(한국어 별칭). key와도 매칭된다. */
13
+ header: string;
14
+ required?: boolean;
15
+ /** 허용값 목록 — 있으면 오류 셀을 Select로 수정 */
16
+ options?: string[];
17
+ }
18
+ interface ExcelUploadCardProps {
19
+ /** server = 파일을 서버로 바로 업로드, preview = 클라이언트 파싱·검증 후 가져오기 */
20
+ mode?: 'server' | 'preview';
21
+ /** server: multipart POST 대상 / preview: 검증된 rows JSON POST 대상 */
22
+ endpoint?: string;
23
+ /** 템플릿 다운로드 GET 경로 (templateColumns 없을 때 사용) */
24
+ templateEndpoint?: string;
25
+ /** 있으면 클라이언트에서 템플릿 xlsx 생성 — 백엔드 없이 동작 */
26
+ templateColumns?: ExcelTemplateColumn[];
27
+ /** preview 모드 파싱·검증 스키마 */
28
+ columns?: ExcelPreviewColumn[];
29
+ /** server 응답 요약 타일 라벨 매핑 (응답 키 → 라벨) */
30
+ summaryLabels?: Record<string, string>;
31
+ /** server: 응답 JSON / preview: 서버 응답(없으면 유효 행 배열) */
32
+ onComplete?: (result: unknown) => void;
33
+ }
34
+ /** 바깥에서 온 `style`·`className`을 뿌리에 얹어 내보낸다 — 자기 축(폭·여백)이 닿게 하는 통로. */
35
+ declare const ExcelUploadCard: React__default.FC<ExcelUploadCardProps & SduiBoxProps>;
36
+
37
+ export { type ExcelPreviewColumn, type ExcelTemplateColumn, ExcelUploadCard, type ExcelUploadCardProps };
@@ -0,0 +1,606 @@
1
+ import { loadOptionalModule } from '../chunk-RRMKCZYQ.js';
2
+ import { useSduiAdapter, useSduiSandbox, hostFetchOf, defaultAdapter, hostRawFetchOf } from '../chunk-DKPNFEVU.js';
3
+ import { Select, Input, Card, Button, Spinner, Icon, Alert, StatCard, Badge, Table, EmptyState } from '../chunk-WIDDHR7L.js';
4
+ import { boxed } from '../chunk-SAN4IX6N.js';
5
+ import { useRef, useState, useMemo, useCallback } from 'react';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
7
+
8
+ var ACCEPT = ".xlsx,.xls";
9
+ function pickCell(raw, column) {
10
+ const keys = Object.keys(raw);
11
+ const match = keys.find((key) => key.trim() === column.header) ?? keys.find((key) => key.trim() === column.key);
12
+ const value = match !== void 0 ? raw[match] : "";
13
+ return value === null || value === void 0 ? "" : String(value).trim();
14
+ }
15
+ function validateCell(column, value) {
16
+ if (value === "") return column.required ? "\uD544\uC218 \uC785\uB825\uC785\uB2C8\uB2E4" : null;
17
+ if (column.options && !column.options.includes(value)) {
18
+ return `\uD5C8\uC6A9\uAC12\uC774 \uC544\uB2D9\uB2C8\uB2E4 (\uD5C8\uC6A9: ${column.options.join(", ")})`;
19
+ }
20
+ return null;
21
+ }
22
+ function formatBytes(bytes) {
23
+ if (bytes < 1024) return `${bytes}B`;
24
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
25
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
26
+ }
27
+ function collectSummary(result, summaryLabels) {
28
+ const nested = result.summary && typeof result.summary === "object" ? result.summary : void 0;
29
+ const lookup = (key) => nested?.[key] ?? result[key];
30
+ if (summaryLabels) {
31
+ return Object.entries(summaryLabels).flatMap(([key, label]) => {
32
+ const value = lookup(key);
33
+ return typeof value === "number" && Number.isFinite(value) ? [{ key, label, value }] : [];
34
+ });
35
+ }
36
+ const source = nested ?? result;
37
+ return Object.entries(source).flatMap(
38
+ ([key, value]) => typeof value === "number" && Number.isFinite(value) ? [{ key, label: key, value }] : []
39
+ );
40
+ }
41
+ function errorMessage(error, fallback) {
42
+ return error instanceof Error && error.message ? error.message : fallback;
43
+ }
44
+ function ExcelUploadCardImpl({
45
+ mode = "server",
46
+ endpoint,
47
+ templateEndpoint,
48
+ templateColumns,
49
+ columns,
50
+ summaryLabels,
51
+ onComplete
52
+ }) {
53
+ const adapter = useSduiAdapter();
54
+ const sandbox = useSduiSandbox();
55
+ const api = hostFetchOf(adapter) ?? defaultAdapter.fetch;
56
+ const rawFetch = hostRawFetchOf(adapter) ?? defaultAdapter.rawFetch;
57
+ const inputRef = useRef(null);
58
+ const [inputKey, setInputKey] = useState(0);
59
+ const [fileName, setFileName] = useState(null);
60
+ const [notice, setNotice] = useState(null);
61
+ const [demoMode, setDemoMode] = useState(false);
62
+ const [downloading, setDownloading] = useState(false);
63
+ const [file, setFile] = useState(null);
64
+ const [uploading, setUploading] = useState(false);
65
+ const [result, setResult] = useState(null);
66
+ const [rows, setRows] = useState(null);
67
+ const [parsing, setParsing] = useState(false);
68
+ const excelRowNums = useRef([]);
69
+ const [importing, setImporting] = useState(false);
70
+ const schemaColumns = useMemo(() => columns ?? [], [columns]);
71
+ const reset = useCallback(() => {
72
+ setFile(null);
73
+ setFileName(null);
74
+ setResult(null);
75
+ setRows(null);
76
+ setNotice(null);
77
+ setInputKey((key) => key + 1);
78
+ }, []);
79
+ const hasTemplate = (templateColumns?.length ?? 0) > 0 || Boolean(templateEndpoint);
80
+ const handleTemplateDownload = useCallback(async () => {
81
+ setDownloading(true);
82
+ try {
83
+ if (templateColumns && templateColumns.length > 0) {
84
+ const XLSX = await loadOptionalModule(() => import('xlsx'), "xlsx");
85
+ const headers = templateColumns.map((column) => column.header);
86
+ const examples = templateColumns.map((column) => column.example ?? "");
87
+ const sheet = XLSX.utils.aoa_to_sheet([headers, examples]);
88
+ sheet["!cols"] = templateColumns.map((column) => ({
89
+ wch: Math.max(column.header.length * 2, (column.example ?? "").length + 2, 12)
90
+ }));
91
+ const workbook = XLSX.utils.book_new();
92
+ XLSX.utils.book_append_sheet(workbook, sheet, "\uC5C5\uB85C\uB4DC\uC591\uC2DD");
93
+ XLSX.writeFile(workbook, "\uC5D1\uC140_\uC5C5\uB85C\uB4DC_\uD15C\uD50C\uB9BF.xlsx");
94
+ } else if (templateEndpoint) {
95
+ const res = await rawFetch(templateEndpoint, { method: "GET" });
96
+ if (res.status === 503) {
97
+ setDemoMode(true);
98
+ setNotice({
99
+ tone: "warning",
100
+ text: "\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 \uC0C1\uD0DC\uB77C \uD15C\uD50C\uB9BF\uC744 \uBC1B\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uB370\uBAA8 \uBAA8\uB4DC)"
101
+ });
102
+ return;
103
+ }
104
+ if (!res.ok) throw new Error(`\uD15C\uD50C\uB9BF \uB2E4\uC6B4\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${res.status})`);
105
+ const blob = await res.blob();
106
+ const url = URL.createObjectURL(blob);
107
+ const link = document.createElement("a");
108
+ link.href = url;
109
+ link.download = "\uC5D1\uC140_\uC5C5\uB85C\uB4DC_\uD15C\uD50C\uB9BF.xlsx";
110
+ document.body.appendChild(link);
111
+ link.click();
112
+ link.remove();
113
+ URL.revokeObjectURL(url);
114
+ }
115
+ } catch (error) {
116
+ setNotice({ tone: "danger", text: errorMessage(error, "\uD15C\uD50C\uB9BF \uB2E4\uC6B4\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4") });
117
+ } finally {
118
+ setDownloading(false);
119
+ }
120
+ }, [templateColumns, templateEndpoint]);
121
+ const handleServerUpload = useCallback(async () => {
122
+ if (!file || !endpoint) return;
123
+ setUploading(true);
124
+ setNotice(null);
125
+ setResult(null);
126
+ try {
127
+ const formData = new FormData();
128
+ formData.append("file", file, file.name);
129
+ if (sandbox) {
130
+ setNotice({ tone: "warning", text: "\uBBF8\uB9AC\uBCF4\uAE30\uC5D0\uC11C\uB294 \uC5C5\uB85C\uB4DC\uAC00 \uB098\uAC00\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4" });
131
+ return;
132
+ }
133
+ const res = await rawFetch(endpoint, { method: "POST", body: formData });
134
+ if (res.status === 503) {
135
+ setDemoMode(true);
136
+ return;
137
+ }
138
+ const json = await res.json().catch(() => null);
139
+ if (!res.ok) {
140
+ if (json) setResult(json);
141
+ setNotice({
142
+ tone: "danger",
143
+ text: typeof json?.message === "string" && json.message || typeof json?.error === "string" && json.error || `\uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${res.status})`
144
+ });
145
+ return;
146
+ }
147
+ setResult(json ?? {});
148
+ setNotice({ tone: "success", text: "\uC5C5\uB85C\uB4DC\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4" });
149
+ setFile(null);
150
+ setFileName(null);
151
+ setInputKey((key) => key + 1);
152
+ onComplete?.(json ?? {});
153
+ } catch (error) {
154
+ setNotice({ tone: "danger", text: errorMessage(error, "\uC5C5\uB85C\uB4DC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4") });
155
+ } finally {
156
+ setUploading(false);
157
+ }
158
+ }, [endpoint, file, onComplete]);
159
+ const validateRow = useCallback(
160
+ (row) => {
161
+ const errors = {};
162
+ for (const column of schemaColumns) {
163
+ const message = validateCell(column, row.cells[column.key] ?? "");
164
+ if (message) errors[column.key] = message;
165
+ }
166
+ return { ...row, errors };
167
+ },
168
+ [schemaColumns]
169
+ );
170
+ const parsePreviewFile = useCallback(
171
+ async (selected) => {
172
+ setParsing(true);
173
+ setNotice(null);
174
+ setRows(null);
175
+ try {
176
+ const buffer = await selected.arrayBuffer();
177
+ const XLSX = await loadOptionalModule(() => import('xlsx'), "xlsx");
178
+ const workbook = XLSX.read(buffer, { type: "array" });
179
+ const sheet = workbook.Sheets[workbook.SheetNames[0]];
180
+ if (!sheet) throw new Error("\uC5D1\uC140 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
181
+ const raw = XLSX.utils.sheet_to_json(sheet, {
182
+ defval: "",
183
+ raw: false
184
+ });
185
+ if (raw.length === 0) {
186
+ setNotice({ tone: "warning", text: "\uB370\uC774\uD130 \uD589\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uD15C\uD50C\uB9BF \uC591\uC2DD\uC744 \uD655\uC778\uD558\uC138\uC694" });
187
+ return;
188
+ }
189
+ excelRowNums.current = raw.map((rawRow, index) => {
190
+ const rowNum = rawRow.__rowNum__;
191
+ return typeof rowNum === "number" ? rowNum + 1 : index + 2;
192
+ });
193
+ setRows(
194
+ raw.map((rawRow, index) => {
195
+ const cells = {};
196
+ for (const column of schemaColumns) cells[column.key] = pickCell(rawRow, column);
197
+ return validateRow({ id: index, cells });
198
+ })
199
+ );
200
+ setFileName(selected.name);
201
+ } catch (error) {
202
+ setNotice({ tone: "danger", text: errorMessage(error, "\uC5D1\uC140 \uD30C\uC77C\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4") });
203
+ } finally {
204
+ setParsing(false);
205
+ }
206
+ },
207
+ [schemaColumns, validateRow]
208
+ );
209
+ const commitCell = useCallback(
210
+ (rowId, key, value) => {
211
+ setRows(
212
+ (current) => current ? current.map(
213
+ (row) => row.id === rowId ? validateRow({ id: row.id, cells: { ...row.cells, [key]: value.trim() } }) : row
214
+ ) : current
215
+ );
216
+ },
217
+ [validateRow]
218
+ );
219
+ const validRows = useMemo(
220
+ () => (rows ?? []).filter((row) => Object.keys(row.errors).length === 0),
221
+ [rows]
222
+ );
223
+ const invalidCount = (rows?.length ?? 0) - validRows.length;
224
+ const handleImport = useCallback(async () => {
225
+ if (validRows.length === 0) return;
226
+ const payload = validRows.map((row) => ({ ...row.cells }));
227
+ setImporting(true);
228
+ setNotice(null);
229
+ try {
230
+ if (endpoint) {
231
+ if (sandbox) {
232
+ setNotice({ tone: "warning", text: "\uBBF8\uB9AC\uBCF4\uAE30\uC5D0\uC11C\uB294 \uB4F1\uB85D\uC774 \uB098\uAC00\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4" });
233
+ return;
234
+ }
235
+ const response = await api(endpoint, { method: "POST", body: { rows: payload } });
236
+ setNotice({ tone: "success", text: `${payload.length}\uAC74\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4` });
237
+ onComplete?.(response ?? payload);
238
+ } else {
239
+ setNotice({ tone: "success", text: `${payload.length}\uAC74\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4` });
240
+ onComplete?.(payload);
241
+ }
242
+ setRows(null);
243
+ setFileName(null);
244
+ setInputKey((key) => key + 1);
245
+ } catch (error) {
246
+ if (error?.status === 503) {
247
+ setDemoMode(true);
248
+ setNotice({
249
+ tone: "warning",
250
+ text: `\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 \u2014 \uAC80\uC99D\uB41C ${payload.length}\uAC74\uC744 \uB85C\uCEEC \uACB0\uACFC\uB85C \uC804\uB2EC\uD588\uC2B5\uB2C8\uB2E4 (\uB370\uBAA8 \uBAA8\uB4DC)`
251
+ });
252
+ onComplete?.(payload);
253
+ setRows(null);
254
+ setFileName(null);
255
+ setInputKey((key) => key + 1);
256
+ } else {
257
+ setNotice({ tone: "danger", text: errorMessage(error, "\uAC00\uC838\uC624\uAE30\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4") });
258
+ }
259
+ } finally {
260
+ setImporting(false);
261
+ }
262
+ }, [endpoint, onComplete, validRows]);
263
+ const previewTableColumns = useMemo(() => {
264
+ const base = [
265
+ {
266
+ key: "_row",
267
+ header: "#",
268
+ width: "48px",
269
+ align: "right",
270
+ mono: true,
271
+ // 엑셀 행 번호(헤더 1행 + 1-기반)
272
+ render: (row) => excelRowNums.current[row.id] ?? row.id + 2
273
+ },
274
+ {
275
+ key: "_status",
276
+ header: "\uC0C1\uD0DC",
277
+ width: "64px",
278
+ render: (row) => Object.keys(row.errors).length === 0 ? /* @__PURE__ */ jsx(Badge, { tone: "success", children: "\uC815\uC0C1" }) : /* @__PURE__ */ jsx(Badge, { tone: "danger", children: "\uC624\uB958" })
279
+ }
280
+ ];
281
+ const dataColumns = schemaColumns.map((column) => ({
282
+ key: column.key,
283
+ header: column.required ? `${column.header} *` : column.header,
284
+ render: (row) => {
285
+ const value = row.cells[column.key] ?? "";
286
+ const error = row.errors[column.key];
287
+ if (!error) {
288
+ return value !== "" ? value : /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-muted)" }, children: "\u2014" });
289
+ }
290
+ return /* @__PURE__ */ jsxs(
291
+ "div",
292
+ {
293
+ className: "wp-col",
294
+ style: {
295
+ gap: "var(--wp-space-half)",
296
+ padding: "var(--wp-space-1)",
297
+ background: "var(--wp-status-danger-bg)",
298
+ borderRadius: "var(--wp-radius-sm)",
299
+ minWidth: 140
300
+ },
301
+ children: [
302
+ column.options ? /* @__PURE__ */ jsx(
303
+ Select,
304
+ {
305
+ size: "sm",
306
+ invalid: true,
307
+ value: "",
308
+ placeholder: value !== "" ? `${value} (\uD5C8\uC6A9\uAC12 \uC544\uB2D8)` : "\uAC12 \uC120\uD0DD",
309
+ "aria-label": `${column.header} \uC218\uC815`,
310
+ options: column.options.map((option) => ({ value: option, label: option })),
311
+ onChange: (event) => {
312
+ if (event.target.value) commitCell(row.id, column.key, event.target.value);
313
+ }
314
+ }
315
+ ) : /* @__PURE__ */ jsx(
316
+ Input,
317
+ {
318
+ size: "sm",
319
+ invalid: true,
320
+ defaultValue: value,
321
+ "aria-label": `${column.header} \uC218\uC815`,
322
+ placeholder: column.header,
323
+ onBlur: (event) => commitCell(row.id, column.key, event.target.value),
324
+ onKeyDown: (event) => {
325
+ if (event.key === "Enter") {
326
+ commitCell(row.id, column.key, event.currentTarget.value);
327
+ }
328
+ }
329
+ }
330
+ ),
331
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { color: "var(--wp-text-danger)" }, children: error })
332
+ ]
333
+ }
334
+ );
335
+ }
336
+ }));
337
+ return [...base, ...dataColumns];
338
+ }, [commitCell, schemaColumns]);
339
+ const handleFileChange = (event) => {
340
+ const selected = event.target.files?.[0];
341
+ if (!selected) return;
342
+ if (mode === "preview") {
343
+ void parsePreviewFile(selected);
344
+ } else {
345
+ setFile(selected);
346
+ setFileName(selected.name);
347
+ setResult(null);
348
+ setNotice(null);
349
+ }
350
+ };
351
+ const previewSchemaMissing = mode === "preview" && schemaColumns.length === 0;
352
+ const pickerDisabled = uploading || parsing || importing || previewSchemaMissing;
353
+ const pickerReason = previewSchemaMissing ? "columns \uC2A4\uD0A4\uB9C8\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 \uBBF8\uB9AC\uBCF4\uAE30\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" : void 0;
354
+ const uploadDisabledReason = !endpoint ? "\uC5C5\uB85C\uB4DC endpoint\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4" : demoMode ? "\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 \uC0C1\uD0DC\uC785\uB2C8\uB2E4 (\uB370\uBAA8 \uBAA8\uB4DC)" : !file ? "\uC5D1\uC140 \uD30C\uC77C\uC744 \uBA3C\uC800 \uC120\uD0DD\uD558\uC138\uC694" : void 0;
355
+ const importDisabledReason = validRows.length === 0 ? "\uAC00\uC838\uC62C \uC218 \uC788\uB294 \uC815\uC0C1 \uD589\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" : void 0;
356
+ const summaryEntries = result ? collectSummary(result, summaryLabels) : [];
357
+ const rowErrors = Array.isArray(result?.errors) ? result.errors : [];
358
+ const hasReset = fileName !== null || file !== null || result !== null || rows !== null || notice !== null;
359
+ return /* @__PURE__ */ jsx(
360
+ Card,
361
+ {
362
+ title: "\uC5D1\uC140 \uC77C\uAD04 \uC5C5\uB85C\uB4DC",
363
+ subtitle: mode === "preview" ? "\uC5D1\uC140 \uD30C\uC77C\uC744 \uBD88\uB7EC\uC640 \uAC80\uC99D\uD55C \uB4A4 \uC815\uC0C1 \uD589\uB9CC \uAC00\uC838\uC635\uB2C8\uB2E4" : "\uC5D1\uC140 \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uBA74 \uC11C\uBC84\uC5D0\uC11C \uC77C\uAD04 \uCC98\uB9AC\uD569\uB2C8\uB2E4",
364
+ actions: hasTemplate ? /* @__PURE__ */ jsx(
365
+ Button,
366
+ {
367
+ variant: "secondary",
368
+ size: "sm",
369
+ icon: "download",
370
+ loading: downloading,
371
+ onClick: () => void handleTemplateDownload(),
372
+ children: "\uD15C\uD50C\uB9BF \uB2E4\uC6B4\uB85C\uB4DC"
373
+ }
374
+ ) : void 0,
375
+ children: /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-3)" }, children: [
376
+ /* @__PURE__ */ jsx(
377
+ "input",
378
+ {
379
+ ref: inputRef,
380
+ type: "file",
381
+ accept: ACCEPT,
382
+ hidden: true,
383
+ onChange: handleFileChange
384
+ },
385
+ inputKey
386
+ ),
387
+ /* @__PURE__ */ jsxs(
388
+ "div",
389
+ {
390
+ style: {
391
+ display: "flex",
392
+ alignItems: "center",
393
+ flexWrap: "wrap",
394
+ gap: "var(--wp-space-2)"
395
+ },
396
+ children: [
397
+ /* @__PURE__ */ jsx(
398
+ Button,
399
+ {
400
+ variant: "secondary",
401
+ icon: "upload",
402
+ disabled: pickerDisabled,
403
+ title: pickerReason,
404
+ onClick: () => inputRef.current?.click(),
405
+ children: "\uD30C\uC77C \uC120\uD0DD"
406
+ }
407
+ ),
408
+ parsing ? /* @__PURE__ */ jsx(Spinner, { size: "sm", label: "\uC5D1\uC140 \uD30C\uC2F1 \uC911\u2026" }) : fileName ? /* @__PURE__ */ jsxs(
409
+ "span",
410
+ {
411
+ className: "wp-body-sm",
412
+ style: {
413
+ display: "inline-flex",
414
+ alignItems: "center",
415
+ gap: "var(--wp-space-1)",
416
+ color: "var(--wp-text-default)"
417
+ },
418
+ children: [
419
+ /* @__PURE__ */ jsx(Icon, { name: "file-text", size: 16, style: { color: "var(--wp-text-muted)" } }),
420
+ fileName,
421
+ file ? /* @__PURE__ */ jsxs("span", { className: "wp-caption", style: { color: "var(--wp-text-muted)" }, children: [
422
+ "(",
423
+ formatBytes(file.size),
424
+ ")"
425
+ ] }) : null
426
+ ]
427
+ }
428
+ ) : /* @__PURE__ */ jsx("span", { className: "wp-caption", children: ".xlsx / .xls \uD30C\uC77C\uC744 \uC120\uD0DD\uD558\uC138\uC694" }),
429
+ /* @__PURE__ */ jsxs("div", { style: { marginLeft: "auto", display: "flex", gap: "var(--wp-space-2)" }, children: [
430
+ /* @__PURE__ */ jsx(
431
+ Button,
432
+ {
433
+ variant: "ghost",
434
+ icon: "rotate-ccw",
435
+ disabled: !hasReset || uploading || importing || parsing,
436
+ title: !hasReset ? "\uCD08\uAE30\uD654\uD560 \uB0B4\uC6A9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" : void 0,
437
+ onClick: reset,
438
+ children: "\uCD08\uAE30\uD654"
439
+ }
440
+ ),
441
+ mode === "server" ? /* @__PURE__ */ jsx(
442
+ Button,
443
+ {
444
+ variant: "primary",
445
+ icon: "cloud-upload",
446
+ loading: uploading,
447
+ disabled: uploadDisabledReason !== void 0,
448
+ title: uploadDisabledReason,
449
+ onClick: () => void handleServerUpload(),
450
+ children: "\uC5C5\uB85C\uB4DC"
451
+ }
452
+ ) : rows !== null ? /* @__PURE__ */ jsxs(
453
+ Button,
454
+ {
455
+ variant: "primary",
456
+ icon: "check",
457
+ loading: importing,
458
+ disabled: importDisabledReason !== void 0,
459
+ title: importDisabledReason,
460
+ onClick: () => void handleImport(),
461
+ children: [
462
+ validRows.length,
463
+ "\uAC74 \uAC00\uC838\uC624\uAE30",
464
+ invalidCount > 0 ? ` (\uC624\uB958 ${invalidCount}\uAC74 \uC81C\uC678)` : ""
465
+ ]
466
+ }
467
+ ) : null
468
+ ] })
469
+ ]
470
+ }
471
+ ),
472
+ demoMode ? /* @__PURE__ */ jsx(Alert, { tone: "warning", title: "\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 (\uB370\uBAA8 \uBAA8\uB4DC)", children: "\uC11C\uBC84 \uCC98\uB9AC\uAC00 \uBD88\uAC00\uB2A5\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uC5F0\uB3D9 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694." }) : null,
473
+ notice ? /* @__PURE__ */ jsx(Alert, { tone: notice.tone, children: notice.text }) : null,
474
+ mode === "server" && summaryEntries.length > 0 ? /* @__PURE__ */ jsx(
475
+ "div",
476
+ {
477
+ style: {
478
+ display: "grid",
479
+ gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
480
+ gap: "var(--wp-space-2)"
481
+ },
482
+ children: summaryEntries.map((entry) => /* @__PURE__ */ jsx(
483
+ StatCard,
484
+ {
485
+ size: "sm",
486
+ label: entry.label,
487
+ value: entry.value.toLocaleString("ko-KR")
488
+ },
489
+ entry.key
490
+ ))
491
+ }
492
+ ) : null,
493
+ mode === "server" && rowErrors.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-1)" }, children: [
494
+ /* @__PURE__ */ jsxs(
495
+ "span",
496
+ {
497
+ className: "wp-body-sm",
498
+ style: { fontWeight: "var(--wp-weight-semibold)", color: "var(--wp-text-strong)" },
499
+ children: [
500
+ "\uC624\uB958 \uBAA9\uB85D (",
501
+ rowErrors.length,
502
+ "\uAC74)"
503
+ ]
504
+ }
505
+ ),
506
+ /* @__PURE__ */ jsx(
507
+ "div",
508
+ {
509
+ style: {
510
+ maxHeight: 240,
511
+ overflowY: "auto",
512
+ border: "var(--wp-border-w) solid var(--wp-border-subtle)",
513
+ borderRadius: "var(--wp-radius-md)",
514
+ padding: "var(--wp-space-2)"
515
+ },
516
+ children: rowErrors.map((rowError, index) => /* @__PURE__ */ jsxs(
517
+ "div",
518
+ {
519
+ className: "wp-body-sm",
520
+ style: {
521
+ display: "flex",
522
+ gap: "var(--wp-space-2)",
523
+ padding: "var(--wp-space-1) 0"
524
+ },
525
+ children: [
526
+ /* @__PURE__ */ jsxs(
527
+ "span",
528
+ {
529
+ style: {
530
+ color: "var(--wp-text-danger)",
531
+ fontWeight: "var(--wp-weight-medium)",
532
+ flexShrink: 0
533
+ },
534
+ children: [
535
+ rowError.row_number ?? rowError.rowNumber,
536
+ "\uD589"
537
+ ]
538
+ }
539
+ ),
540
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-default)" }, children: rowError.message })
541
+ ]
542
+ },
543
+ `${rowError.row_number ?? rowError.rowNumber}-${index}`
544
+ ))
545
+ }
546
+ )
547
+ ] }) : null,
548
+ mode === "preview" && rows !== null ? /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-2)" }, children: [
549
+ /* @__PURE__ */ jsxs(
550
+ "div",
551
+ {
552
+ style: {
553
+ display: "flex",
554
+ alignItems: "center",
555
+ gap: "var(--wp-space-2)",
556
+ flexWrap: "wrap"
557
+ },
558
+ children: [
559
+ /* @__PURE__ */ jsxs(Badge, { tone: "neutral", children: [
560
+ "\uCD1D ",
561
+ rows.length,
562
+ "\uAC74"
563
+ ] }),
564
+ /* @__PURE__ */ jsxs(Badge, { tone: "success", dot: true, children: [
565
+ "\uC815\uC0C1 ",
566
+ validRows.length,
567
+ "\uAC74"
568
+ ] }),
569
+ invalidCount > 0 ? /* @__PURE__ */ jsxs(Badge, { tone: "danger", dot: true, children: [
570
+ "\uC624\uB958 ",
571
+ invalidCount,
572
+ "\uAC74"
573
+ ] }) : null,
574
+ invalidCount > 0 ? /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uC624\uB958 \uC140\uC744 \uD45C\uC5D0\uC11C \uBC14\uB85C \uC218\uC815\uD558\uBA74 \uC7AC\uAC80\uC99D\uB429\uB2C8\uB2E4" }) : null
575
+ ]
576
+ }
577
+ ),
578
+ /* @__PURE__ */ jsx(
579
+ "div",
580
+ {
581
+ style: {
582
+ maxHeight: 360,
583
+ overflow: "auto",
584
+ border: "var(--wp-border-w) solid var(--wp-border-subtle)",
585
+ borderRadius: "var(--wp-radius-md)"
586
+ },
587
+ children: /* @__PURE__ */ jsx(
588
+ Table,
589
+ {
590
+ columns: previewTableColumns,
591
+ rows,
592
+ dense: true,
593
+ sticky: true,
594
+ empty: /* @__PURE__ */ jsx(EmptyState, { icon: "table", title: "\uD45C\uC2DC\uD560 \uD589\uC774 \uC5C6\uC2B5\uB2C8\uB2E4", tight: true, children: "\uC5D1\uC140 \uD30C\uC77C\uC5D0 \uB370\uC774\uD130 \uD589\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." })
595
+ }
596
+ )
597
+ }
598
+ )
599
+ ] }) : null
600
+ ] })
601
+ }
602
+ );
603
+ }
604
+ var ExcelUploadCard = boxed(ExcelUploadCardImpl, "ExcelUploadCard");
605
+
606
+ export { ExcelUploadCard };
package/dist/rich.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useOptionalModule, DependencyPlaceholder } from './chunk-WXOU7QTN.js';
2
- export { TimePicker } from './chunk-WXOU7QTN.js';
1
+ export { TimePicker } from './chunk-SNRKPEWT.js';
2
+ import { useOptionalModule, DependencyPlaceholder } from './chunk-RRMKCZYQ.js';
3
3
  import { HoistedStyle } from './chunk-EFMKWPZ3.js';
4
4
  export { DatePicker, DateRangePicker } from './chunk-EFMKWPZ3.js';
5
5
  import './chunk-XQPTSEJC.js';
@@ -1,7 +1,11 @@
1
+ import { Timeline } from '../chunk-EFYEOT47.js';
2
+ export { Timeline } from '../chunk-EFYEOT47.js';
1
3
  import { TreePicker } from '../chunk-U3HYQCFA.js';
2
4
  export { TreePicker } from '../chunk-U3HYQCFA.js';
3
5
  import { TreeView } from '../chunk-X5R2HHEN.js';
4
6
  export { TreeView, buildTreeNodes } from '../chunk-X5R2HHEN.js';
7
+ import { RecordSheet } from '../chunk-K3JBSWFI.js';
8
+ export { RecordSheet } from '../chunk-K3JBSWFI.js';
5
9
  import { RoleMemberPicker } from '../chunk-WXZDYDXG.js';
6
10
  export { RoleMemberPicker } from '../chunk-WXZDYDXG.js';
7
11
  import { SearchableMultiPicker } from '../chunk-C53NGSA3.js';
@@ -14,8 +18,8 @@ import { Strip } from '../chunk-ZH5MLOZS.js';
14
18
  export { Strip } from '../chunk-ZH5MLOZS.js';
15
19
  import { SummaryTable } from '../chunk-O4U7Y3XL.js';
16
20
  export { SummaryTable } from '../chunk-O4U7Y3XL.js';
17
- import { Timeline } from '../chunk-EFYEOT47.js';
18
- export { Timeline } from '../chunk-EFYEOT47.js';
21
+ import { JsonField } from '../chunk-JVBZRDC2.js';
22
+ export { JsonField } from '../chunk-JVBZRDC2.js';
19
23
  import { LiveClock } from '../chunk-Z3WGIFJP.js';
20
24
  export { LiveClock } from '../chunk-Z3WGIFJP.js';
21
25
  import { OrgChart } from '../chunk-QWJ3OBBL.js';
@@ -30,8 +34,7 @@ import { PrintSheet } from '../chunk-MYJ2KEIP.js';
30
34
  export { PrintSheet } from '../chunk-MYJ2KEIP.js';
31
35
  import { RankBoard } from '../chunk-U6H7YN4I.js';
32
36
  export { RankBoard } from '../chunk-U6H7YN4I.js';
33
- import { RecordSheet } from '../chunk-K3JBSWFI.js';
34
- export { RecordSheet } from '../chunk-K3JBSWFI.js';
37
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-CVCP52OD.js';
35
38
  import { ColumnBoard } from '../chunk-C4JRSZGS.js';
36
39
  export { ColumnBoard } from '../chunk-C4JRSZGS.js';
37
40
  import { DiffView } from '../chunk-LDJ2CFFH.js';
@@ -45,8 +48,6 @@ import { ImageLightbox } from '../chunk-XPBZMECW.js';
45
48
  export { ImageLightbox } from '../chunk-XPBZMECW.js';
46
49
  import { InlineEdit } from '../chunk-KT3ZVBLN.js';
47
50
  export { InlineEdit } from '../chunk-KT3ZVBLN.js';
48
- import { JsonField } from '../chunk-JVBZRDC2.js';
49
- export { JsonField } from '../chunk-JVBZRDC2.js';
50
51
  import '../chunk-QU7YVMFA.js';
51
52
  import '../chunk-THEP3VWY.js';
52
53
  import { ApprovalOverridesEditor } from '../chunk-OR27SRKL.js';
@@ -55,13 +56,12 @@ import { TagMultiSelect } from '../chunk-QDEZXA34.js';
55
56
  export { TagMultiSelect } from '../chunk-QDEZXA34.js';
56
57
  import { ApprovalLineEditor } from '../chunk-S4LOH2TU.js';
57
58
  export { ApprovalLineEditor } from '../chunk-S4LOH2TU.js';
58
- import { ApprovalStepper } from '../chunk-ZTQEP3TR.js';
59
- export { ApprovalStepper } from '../chunk-ZTQEP3TR.js';
60
- export { AutocompleteField, toAutocompleteOptions } from '../chunk-CVCP52OD.js';
61
59
  import '../chunk-BD75U73S.js';
62
60
  import '../chunk-EFMKWPZ3.js';
63
61
  import '../chunk-XQPTSEJC.js';
64
62
  import '../chunk-DKPNFEVU.js';
63
+ import { ApprovalStepper } from '../chunk-ZTQEP3TR.js';
64
+ export { ApprovalStepper } from '../chunk-ZTQEP3TR.js';
65
65
  import '../chunk-WIDDHR7L.js';
66
66
  import '../chunk-SAN4IX6N.js';
67
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",
@@ -32,6 +32,10 @@
32
32
  "types": "./dist/rich.d.ts",
33
33
  "default": "./dist/rich.js"
34
34
  },
35
+ "./rich/ExcelUploadCard": {
36
+ "types": "./dist/rich/ExcelUploadCard.d.ts",
37
+ "default": "./dist/rich/ExcelUploadCard.js"
38
+ },
35
39
  "./widgets": {
36
40
  "types": "./dist/widgets/index.d.ts",
37
41
  "default": "./dist/widgets/index.js"