@wipco/sdui 0.30.0 → 0.32.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,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
+ import { useOptionalModule, DependencyPlaceholder } from './chunk-RRMKCZYQ.js';
2
+ export { TimePicker } from './chunk-SNRKPEWT.js';
3
3
  import { HoistedStyle } from './chunk-EFMKWPZ3.js';
4
4
  export { DatePicker, DateRangePicker } from './chunk-EFMKWPZ3.js';
5
5
  import './chunk-XQPTSEJC.js';
@@ -0,0 +1,63 @@
1
+ import * as React from 'react';
2
+ import { S as SduiBoxProps } from '../layout-B1QrIBm6.js';
3
+
4
+ /**
5
+ * OptionSchemaEditor — 장비가 자기 유닛에게 받을 **옵션 서식**을 정한다.
6
+ *
7
+ * 유닛의 사양(CPU·RAM·해상도…)은 장비 종류마다 달라서 컬럼으로 세울 수 없다.
8
+ * 그래서 값은 `asset_units.config`(JSON)에 담기는데, 지금까지는 그 JSON을 사람이
9
+ * 직접 쓰고 있었다 — 키 이름이 유닛마다 갈리고(`cpu`/`CPU`/`씨피유`) 숫자여야 할
10
+ * 자리에 문자열이 들어가도 아무도 못 막는다.
11
+ *
12
+ * 여기서 정하는 것은 **값이 아니라 서식**이다. 장비에 한 번 정해 두면
13
+ * (`assets.config.options`) 유닛 수정 화면이 그 서식대로 칸을 그린다
14
+ * ({@link ./OptionValueFields.tsx}). SDUI가 스펙을 화면으로 만드는 것과 같은 결이고,
15
+ * 서식이 스펙이 아니라 **데이터**라는 점만 다르다 — 장비마다 다르니 스펙일 수 없다.
16
+ *
17
+ * 계약: `value`는 장비의 **config 객체 통째**이고 서식은 그 안의 `options`다.
18
+ * { options: [{ key, label?, type: 'text'|'number'|'select'|'boolean', options?, unit? }] }
19
+ *
20
+ * 배열만 주고받지 않는 이유는 SDUI의 `setState`가 **점 경로를 평면 키로 저장**하기
21
+ * 때문이다 — `assetEditForm.config.options`로 쓰면 그 이름의 납작한 키가 하나 생길 뿐
22
+ * `config` 안에 들어가지 않아 폼 본문에 실리지 않는다. 그래서 다른 폼 칸과 똑같이
23
+ * 키 하나(`assetEditForm.config`)를 주고받고, `options` 밖의 값은 그대로 보존한다.
24
+ * - `key`가 유닛 config의 실제 키다. 비었거나 겹치면 저장 못 하게 사유를 띄운다
25
+ * (스펙은 이 위젯 밖이라 검증을 여기서 보여 준다).
26
+ * - `label`은 화면에 쓸 이름 — 비면 key를 그대로 쓴다.
27
+ */
28
+ declare const OPTION_TYPES: readonly [{
29
+ readonly value: "text";
30
+ readonly label: "문자열";
31
+ }, {
32
+ readonly value: "number";
33
+ readonly label: "숫자";
34
+ }, {
35
+ readonly value: "select";
36
+ readonly label: "선택";
37
+ }, {
38
+ readonly value: "boolean";
39
+ readonly label: "예/아니오";
40
+ }];
41
+ type OptionFieldType = (typeof OPTION_TYPES)[number]['value'];
42
+ interface OptionField {
43
+ key: string;
44
+ label?: string;
45
+ type: OptionFieldType;
46
+ /** type=select일 때의 선택지 */
47
+ options?: string[];
48
+ /** 숫자 뒤에 붙는 단위 표시 — GB·인치처럼 값에 섞지 않을 것들 */
49
+ unit?: string;
50
+ }
51
+ /** 장비의 config — 서식은 options에 담고 나머지 키는 손대지 않는다 */
52
+ interface AssetOptionConfig {
53
+ options?: OptionField[];
54
+ [key: string]: unknown;
55
+ }
56
+ interface OptionSchemaEditorProps {
57
+ value?: AssetOptionConfig | null;
58
+ onChange?: (config: AssetOptionConfig) => void;
59
+ disabled?: boolean;
60
+ }
61
+ declare const OptionSchemaEditor: React.FC<OptionSchemaEditorProps & SduiBoxProps>;
62
+
63
+ export { type AssetOptionConfig, OPTION_TYPES, type OptionField, type OptionFieldType, OptionSchemaEditor, type OptionSchemaEditorProps };
@@ -0,0 +1,4 @@
1
+ export { OPTION_TYPES, OptionSchemaEditor } from '../chunk-TIY4A223.js';
2
+ import '../chunk-QO6UJAQN.js';
3
+ import '../chunk-WIDDHR7L.js';
4
+ import '../chunk-SAN4IX6N.js';