@wipco/sdui-editor 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1289 @@
1
+ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { defaultRegistry, Alert, Button, Card, Input, Badge, EmptyState, Spinner, Table, IconButton, SduiDialog, Field, Select, validateSpec, SduiRenderer } from '@wipco/sdui';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { EditorView, basicSetup } from 'codemirror';
5
+ import { EditorState } from '@codemirror/state';
6
+ import { keymap } from '@codemirror/view';
7
+ import { indentWithTab } from '@codemirror/commands';
8
+ import { json } from '@codemirror/lang-json';
9
+
10
+ // src/SduiEditor.tsx
11
+
12
+ // src/store/types.ts
13
+ var PageIdConflictError = class extends Error {
14
+ constructor(existingId) {
15
+ super(`\uB300\uC18C\uBB38\uC790\uB9CC \uB2E4\uB978 \uD398\uC774\uC9C0\uAC00 \uC774\uBBF8 \uC788\uC2B5\uB2C8\uB2E4: ${existingId}`);
16
+ this.existingId = existingId;
17
+ this.name = "PageIdConflictError";
18
+ }
19
+ existingId;
20
+ };
21
+ var PageExistsError = class extends Error {
22
+ constructor(id) {
23
+ super(`\uC774\uBBF8 \uC788\uB294 \uD398\uC774\uC9C0\uC785\uB2C8\uB2E4: ${id}`);
24
+ this.id = id;
25
+ this.name = "PageExistsError";
26
+ }
27
+ id;
28
+ };
29
+ var VersionMismatchError = class extends Error {
30
+ constructor(current) {
31
+ super("\uB2E4\uB978 \uACF3\uC5D0\uC11C \uBA3C\uC800 \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8\uB85C\uACE0\uCE68 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.");
32
+ this.current = current;
33
+ this.name = "VersionMismatchError";
34
+ }
35
+ current;
36
+ };
37
+ var PageGoneError = class extends Error {
38
+ constructor(id) {
39
+ super("\uC218\uC815\uD558\uB824\uB358 \uD398\uC774\uC9C0\uAC00 \uC774\uBBF8 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8\uB85C \uB9CC\uB4E4\uB824\uBA74 \uC0C8 \uD398\uC774\uC9C0\uB85C \uC800\uC7A5\uD558\uC138\uC694.");
40
+ this.id = id;
41
+ this.name = "PageGoneError";
42
+ }
43
+ id;
44
+ };
45
+ var StoreUnsupportedError = class extends Error {
46
+ constructor(operation, detail) {
47
+ super(detail);
48
+ this.operation = operation;
49
+ this.name = "StoreUnsupportedError";
50
+ }
51
+ operation;
52
+ };
53
+ var StoreUnavailableError = class extends Error {
54
+ constructor(message, cause) {
55
+ super(message);
56
+ this.cause = cause;
57
+ this.name = "StoreUnavailableError";
58
+ }
59
+ cause;
60
+ };
61
+ var StoreUnauthorizedError = class extends Error {
62
+ constructor(message) {
63
+ super(message);
64
+ this.name = "StoreUnauthorizedError";
65
+ }
66
+ };
67
+ var PageIdRejectedError = class extends Error {
68
+ constructor(id, detail) {
69
+ super(detail);
70
+ this.id = id;
71
+ this.name = "PageIdRejectedError";
72
+ }
73
+ id;
74
+ };
75
+ var PAGE_ID_PATTERN = /^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$/;
76
+ function isValidPageId(id) {
77
+ return PAGE_ID_PATTERN.test(id) && id.length <= 200;
78
+ }
79
+ function titleOf(spec) {
80
+ if (spec && typeof spec === "object" && typeof spec.title === "string") {
81
+ return spec.title;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ // src/store/rest-store.ts
87
+ function unquoteEtag(value) {
88
+ if (!value) return "";
89
+ return value.trim().replace(/^W\//, "").replace(/^"|"$/g, "");
90
+ }
91
+ function isServableId(id) {
92
+ return id.split("/").every((segment) => !segment.startsWith("_"));
93
+ }
94
+ var NAV_HINT = "`_` \uB85C \uC2DC\uC791\uD558\uB294 id\uB294 ERP\uC5D0\uC11C \uD398\uC774\uC9C0 \uC2A4\uD399\uC774 \uC544\uB2C8\uB77C \uB0B4\uBD80 \uB370\uC774\uD130(_nav \uB4F1)\uC774\uBA70 \uC774 \uACBD\uB85C\uB85C\uB294 \uD3B8\uC9D1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
95
+ var RestSpecStore = class {
96
+ capabilities;
97
+ baseUrl;
98
+ timeoutMs;
99
+ config;
100
+ constructor(config = {}) {
101
+ this.config = config;
102
+ this.baseUrl = (config.baseUrl ?? "").replace(/\/+$/, "");
103
+ this.timeoutMs = config.timeoutMs ?? 1e4;
104
+ this.capabilities = {
105
+ kind: "rest",
106
+ label: config.label ?? (this.baseUrl ? `ERP (${this.baseUrl})` : "ERP (\uAC19\uC740 \uC624\uB9AC\uC9C4)"),
107
+ remove: false,
108
+ atomicCreate: false,
109
+ atomicLocking: false,
110
+ history: false,
111
+ limitations: [
112
+ "ERP\uC5D0\uB294 \uC2A4\uD399 \uC0AD\uC81C \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD398\uC774\uC9C0\uB97C \uC9C0\uC6B8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
113
+ "ERP\uC758 PUT\uC740 If-Match\uB97C \uAC80\uC0AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC800\uC7A5 \uCDA9\uB3CC \uAC80\uC0AC\uB294 \uD3B8\uC9D1\uAE30 \uCABD \uC77D\uAE30\u2192\uBE44\uAD50\uB85C\uB9CC \uC774\uB904\uC838 \uC544\uC8FC \uC9E7\uC740 \uACBD\uD569 \uAD6C\uAC04\uC774 \uB0A8\uC2B5\uB2C8\uB2E4.",
114
+ "\uB0B4\uBE44\uAC8C\uC774\uC158(_nav)\uC740 \uBCC4\uB3C4 \uACBD\uB85C(/ui/nav-spec)\uB77C \uC774 \uBAA9\uB85D\uC5D0 \uB098\uC624\uC9C0 \uC54A\uACE0 \uC5EC\uAE30\uC11C \uC5F4 \uC218\uB3C4 \uC5C6\uC2B5\uB2C8\uB2E4.",
115
+ "\uC774\uB825\uC740 ERP\uAC00 \uC790\uB3D9\uC73C\uB85C \uC313\uC9C0\uB9CC(ui_spec_revisions) \uC870\uD68C\uD560 HTTP \uACBD\uB85C\uAC00 \uC5C6\uC5B4 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uBCFC \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
116
+ "\uAD8C\uD55C\uBCC4 \uC2A4\uD399 \uC7AC\uB2E8\uC740 ERP \uB7F0\uD0C0\uC784\uC758 \uBAAB\uC785\uB2C8\uB2E4 \u2014 \uD3B8\uC9D1\uAE30 \uBBF8\uB9AC\uBCF4\uAE30\uB294 \uC7AC\uB2E8 \uC804 \uC6D0\uBCF8\uC744 \uBCF4\uC5EC \uC90D\uB2C8\uB2E4."
117
+ ]
118
+ };
119
+ }
120
+ /* ---------------------------------------------------------------------- */
121
+ /* HTTP */
122
+ /* ---------------------------------------------------------------------- */
123
+ async request(path, init = {}) {
124
+ const extra = await this.config.headers?.() ?? {};
125
+ let response;
126
+ try {
127
+ response = await fetch(`${this.baseUrl}${path}`, {
128
+ method: init.method ?? "GET",
129
+ headers: {
130
+ accept: "application/json",
131
+ ...init.body !== void 0 ? { "content-type": "application/json" } : null,
132
+ ...extra
133
+ },
134
+ credentials: this.config.credentials ?? "same-origin",
135
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
136
+ // 상류가 응답하지 않으면 편집기가 영원히 매달린다 — 타임아웃을 반드시 건다
137
+ signal: AbortSignal.timeout(this.timeoutMs)
138
+ });
139
+ } catch (err) {
140
+ throw new StoreUnavailableError(
141
+ `\uC2A4\uD399 \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${this.capabilities.label}). \uC11C\uBC84\uAC00 \uB5A0 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.`,
142
+ err
143
+ );
144
+ }
145
+ if (response.status === 401) {
146
+ this.config.onUnauthorized?.();
147
+ throw new StoreUnauthorizedError("\uC138\uC158\uC774 \uB9CC\uB8CC\uB418\uC5C8\uAC70\uB098 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uB85C\uADF8\uC778\uD55C \uB4A4 \uC2DC\uB3C4\uD558\uC138\uC694.");
148
+ }
149
+ if (response.status === 403) {
150
+ throw new StoreUnavailableError(
151
+ "\uC774 \uACC4\uC815\uC5D0\uB294 \uC2A4\uD399 \uD3B8\uC9D1 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (403). ERP \uAD8C\uD55C 'setting.update'\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
152
+ );
153
+ }
154
+ const text = await response.text();
155
+ let body = null;
156
+ if (text.trim()) {
157
+ try {
158
+ body = JSON.parse(text);
159
+ } catch {
160
+ }
161
+ }
162
+ return { status: response.status, etag: unquoteEtag(response.headers.get("etag")), body, text };
163
+ }
164
+ /** 상류 오류를 사람이 읽을 수 있는 문장으로 — 상류가 준 메시지가 있으면 그대로 싣는다 */
165
+ upstreamError(action, response) {
166
+ const payload = response.body;
167
+ const detail = payload && typeof payload === "object" && typeof payload.message === "string" ? payload.message : payload && typeof payload === "object" && typeof payload.error === "string" ? payload.error : response.text.slice(0, 200);
168
+ return new StoreUnavailableError(`${action} \uC2E4\uD328 (${response.status})${detail ? `: ${detail}` : ""}`);
169
+ }
170
+ /* ---------------------------------------------------------------------- */
171
+ /* SpecStore 구현 */
172
+ /* ---------------------------------------------------------------------- */
173
+ async list() {
174
+ const indexed = await this.request("/ui/pages-index");
175
+ if (indexed.status === 200) {
176
+ const pages = indexed.body?.pages;
177
+ if (Array.isArray(pages)) {
178
+ return pages.map((row) => {
179
+ const page = row;
180
+ return {
181
+ id: String(page.id ?? ""),
182
+ title: typeof page.title === "string" ? page.title : null,
183
+ updatedAt: typeof page.updatedAt === "string" ? page.updatedAt : "",
184
+ version: typeof page.etag === "string" ? page.etag : ""
185
+ };
186
+ }).filter((page) => page.id && isValidPageId(page.id)).sort((a, b) => a.id.localeCompare(b.id));
187
+ }
188
+ }
189
+ if (indexed.status !== 404) throw this.upstreamError("\uBAA9\uB85D \uC870\uD68C", indexed);
190
+ const manifest = await this.request("/ui/manifest");
191
+ if (manifest.status !== 200) throw this.upstreamError("\uB9E4\uB2C8\uD398\uC2A4\uD2B8 \uC870\uD68C", manifest);
192
+ const specs = manifest.body?.specs ?? {};
193
+ return Object.entries(specs).filter(([id]) => isValidPageId(id)).map(([id, etag]) => ({
194
+ id,
195
+ title: null,
196
+ updatedAt: "",
197
+ version: typeof etag === "string" ? etag : ""
198
+ })).sort((a, b) => a.id.localeCompare(b.id));
199
+ }
200
+ async read(id) {
201
+ if (!isValidPageId(id)) return null;
202
+ if (!isServableId(id)) throw new PageIdRejectedError(id, NAV_HINT);
203
+ const response = await this.request(`/ui/pages/${id}`);
204
+ if (response.status === 404) return null;
205
+ if (response.status !== 200) throw this.upstreamError(`\uD398\uC774\uC9C0 \uC870\uD68C(${id})`, response);
206
+ return {
207
+ id,
208
+ title: titleOf(response.body),
209
+ updatedAt: "",
210
+ version: response.etag,
211
+ spec: response.body
212
+ };
213
+ }
214
+ async write(id, spec, options = {}) {
215
+ if (!isValidPageId(id)) throw new Error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD398\uC774\uC9C0 id\uC785\uB2C8\uB2E4");
216
+ if (!isServableId(id)) throw new PageIdRejectedError(id, NAV_HINT);
217
+ if (options.ifAbsent || options.expect) {
218
+ const current = await this.read(id);
219
+ if (options.ifAbsent) {
220
+ if (current) throw new PageExistsError(id);
221
+ } else if (options.expect) {
222
+ if (!current) throw new PageGoneError(id);
223
+ if (current.version !== options.expect.value) {
224
+ const { spec: _spec, ...meta } = current;
225
+ throw new VersionMismatchError(meta);
226
+ }
227
+ }
228
+ }
229
+ const title = titleOf(spec);
230
+ const response = await this.request(`/ui/pages/${id}`, {
231
+ method: "PUT",
232
+ // title은 ERP가 목록(pages-index)에 쓰는 별도 컬럼이다 — 스펙의 title과
233
+ // 어긋나면 편집기 목록과 ERP 목록이 갈리므로 저장할 때마다 함께 보낸다.
234
+ body: title === null ? { spec } : { spec, title }
235
+ });
236
+ if (response.status === 404) {
237
+ throw new PageIdRejectedError(id, `\uC2A4\uD399 \uC11C\uBC84\uAC00 \uC774 id\uB97C \uBC1B\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${id}`);
238
+ }
239
+ if (response.status !== 200) throw this.upstreamError(`\uD398\uC774\uC9C0 \uC800\uC7A5(${id})`, response);
240
+ const body = response.body ?? {};
241
+ return {
242
+ id,
243
+ title,
244
+ updatedAt: typeof body.updatedAt === "string" ? body.updatedAt : "",
245
+ // 응답 헤더 ETag와 본문 etag는 같은 값이다(ERP가 저장된 jsonb를 되읽어 만든다)
246
+ version: response.etag || (typeof body.etag === "string" ? body.etag : ""),
247
+ spec
248
+ };
249
+ }
250
+ async remove(id) {
251
+ throw new StoreUnsupportedError(
252
+ "delete",
253
+ `ERP\uC5D0\uB294 \uC2A4\uD399 \uC0AD\uC81C \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (DELETE /ui/pages/${id} \u2192 405). \uD654\uBA74\uC744 \uAC10\uCD94\uB824\uBA74 ERP\uC758 \uB0B4\uBE44\uAC8C\uC774\uC158\xB7\uAD8C\uD55C \uC124\uC815\uC744 \uC4F0\uC138\uC694.`
254
+ );
255
+ }
256
+ // history()는 구현하지 않는다 — ERP가 ui_spec_revisions를 쌓지만 읽는 경로를 열지 않았다.
257
+ };
258
+ function createRestSpecStore(config = {}) {
259
+ return new RestSpecStore(config);
260
+ }
261
+
262
+ // src/templates.ts
263
+ var BLANK = {
264
+ version: 1,
265
+ title: "\uC0C8 \uD398\uC774\uC9C0",
266
+ subtitle: "",
267
+ body: [
268
+ {
269
+ component: "Card",
270
+ children: [{ component: "Text", props: { value: "\uC5EC\uAE30\uC11C\uBD80\uD130 \uC2DC\uC791\uD558\uC138\uC694." } }]
271
+ }
272
+ ]
273
+ };
274
+ var DASHBOARD = {
275
+ version: 1,
276
+ title: "\uB300\uC2DC\uBCF4\uB4DC",
277
+ body: [
278
+ {
279
+ component: "Grid",
280
+ props: { columns: 4 },
281
+ children: [
282
+ { component: "StatCard", props: { label: "\uC9C0\uD45C A", value: "128", unit: "\uAC74", delta: "+4", trend: "up" } },
283
+ { component: "StatCard", props: { label: "\uC9C0\uD45C B", value: "23", unit: "\uAC74", trend: "flat" } },
284
+ { component: "StatCard", props: { label: "\uC9C0\uD45C C", value: "86", unit: "%", delta: "-5%", trend: "down" } },
285
+ { component: "StatCard", props: { label: "\uC9C0\uD45C D", value: "94.2", unit: "%", trend: "flat" } }
286
+ ]
287
+ },
288
+ {
289
+ component: "Card",
290
+ props: { title: "\uC548\uB0B4" },
291
+ children: [{ component: "Text", props: { value: "StatCard\xB7Grid\uB85C \uAD6C\uC131\uD55C \uB300\uC2DC\uBCF4\uB4DC \uACE8\uACA9\uC785\uB2C8\uB2E4." } }]
292
+ }
293
+ ]
294
+ };
295
+ var FORM = {
296
+ version: 1,
297
+ title: "\uC785\uB825 \uD3FC",
298
+ body: [
299
+ {
300
+ component: "Form",
301
+ props: {
302
+ id: "form1",
303
+ initial: { name: "" },
304
+ labels: { name: "\uC774\uB984" },
305
+ rules: { name: [{ rule: "required" }] },
306
+ onSubmit: {
307
+ action: "submitForm",
308
+ form: "form1",
309
+ endpoint: "/api/example",
310
+ onSuccess: [{ action: "toast", tone: "success", title: "\uC800\uC7A5 \uC644\uB8CC" }]
311
+ }
312
+ },
313
+ children: [
314
+ {
315
+ component: "Card",
316
+ props: { title: "\uAE30\uBCF8 \uC815\uBCF4" },
317
+ slots: {
318
+ footer: [
319
+ {
320
+ component: "Button",
321
+ props: {
322
+ variant: "primary",
323
+ loading: { $state: "form1.$submitting", default: false },
324
+ onClick: {
325
+ action: "submitForm",
326
+ form: "form1",
327
+ endpoint: "/api/example",
328
+ onSuccess: [{ action: "toast", tone: "success", title: "\uC800\uC7A5 \uC644\uB8CC" }]
329
+ }
330
+ },
331
+ children: ["\uC800\uC7A5"]
332
+ }
333
+ ]
334
+ },
335
+ children: [
336
+ {
337
+ component: "Field",
338
+ props: { label: "\uC774\uB984", required: true, error: { $state: "form1.$errors.name" } },
339
+ children: [
340
+ {
341
+ component: "Input",
342
+ props: {
343
+ value: { $state: "form1.name", default: "" },
344
+ onChange: { action: "setState", key: "form1.name", fromEvent: true }
345
+ }
346
+ }
347
+ ]
348
+ }
349
+ ]
350
+ }
351
+ ]
352
+ }
353
+ ]
354
+ };
355
+ var LIST = {
356
+ version: 1,
357
+ title: "\uBAA9\uB85D",
358
+ body: [
359
+ {
360
+ component: "Card",
361
+ props: { padding: "none" },
362
+ children: [
363
+ {
364
+ component: "Table",
365
+ props: {
366
+ columns: [
367
+ { key: "id", header: "ID", width: "100px", mono: true },
368
+ { key: "name", header: "\uC774\uB984", strong: true },
369
+ {
370
+ key: "status",
371
+ header: "\uC0C1\uD0DC",
372
+ width: "110px",
373
+ badge: { toneMap: { on: "success", off: "neutral" }, labelMap: { on: "\uD65C\uC131", off: "\uBE44\uD65C\uC131" } }
374
+ }
375
+ ],
376
+ rows: [
377
+ { id: "A-01", name: "\uD56D\uBAA9 1", status: "on" },
378
+ { id: "A-02", name: "\uD56D\uBAA9 2", status: "off" }
379
+ ]
380
+ }
381
+ }
382
+ ]
383
+ }
384
+ ]
385
+ };
386
+ var TEMPLATES = [
387
+ { key: "blank", label: "\uBE48 \uD398\uC774\uC9C0", description: "\uCE74\uB4DC \uD558\uB098\uB85C \uC2DC\uC791", spec: BLANK },
388
+ { key: "dashboard", label: "\uB300\uC2DC\uBCF4\uB4DC", description: "StatCard 4\uAC1C + \uCE74\uB4DC", spec: DASHBOARD },
389
+ { key: "form", label: "\uC785\uB825 \uD3FC", description: "\uAC80\uC99D + \uC81C\uCD9C \uACE8\uACA9", spec: FORM },
390
+ { key: "list", label: "\uBAA9\uB85D", description: "\uD14C\uC774\uBE14 + \uC0C1\uD0DC \uBC30\uC9C0", spec: LIST }
391
+ ];
392
+
393
+ // src/util.ts
394
+ function formatDate(iso) {
395
+ if (!iso) return "";
396
+ try {
397
+ return new Intl.DateTimeFormat("ko-KR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(iso));
398
+ } catch {
399
+ return iso;
400
+ }
401
+ }
402
+ function messageOf(err, fallback) {
403
+ return err instanceof Error && err.message ? err.message : fallback;
404
+ }
405
+ function PageListView({ store, onEdit, onPreview }) {
406
+ const caps = store.capabilities;
407
+ const [pages, setPages] = useState(null);
408
+ const [listFailed, setListFailed] = useState(false);
409
+ const [error, setError] = useState(null);
410
+ const [query, setQuery] = useState("");
411
+ const [createOpen, setCreateOpen] = useState(false);
412
+ const [newId, setNewId] = useState("");
413
+ const [template, setTemplate] = useState("blank");
414
+ const [createError, setCreateError] = useState(null);
415
+ const [creating, setCreating] = useState(false);
416
+ const load = useCallback(() => {
417
+ setListFailed(false);
418
+ store.list().then(setPages).catch((err) => {
419
+ setListFailed(true);
420
+ setError(messageOf(err, "\uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"));
421
+ });
422
+ }, [store]);
423
+ useEffect(load, [load]);
424
+ const filtered = useMemo(() => {
425
+ if (!pages) return null;
426
+ const q = query.trim().toLowerCase();
427
+ if (!q) return pages;
428
+ return pages.filter((p) => p.id.toLowerCase().includes(q) || (p.title ?? "").toLowerCase().includes(q));
429
+ }, [pages, query]);
430
+ const createPage = async () => {
431
+ if (pages === null) {
432
+ setCreateError("\uD398\uC774\uC9C0 \uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD574 \uC911\uBCF5 \uC5EC\uBD80\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBAA9\uB85D\uC744 \uB2E4\uC2DC \uBD88\uB7EC\uC628 \uB4A4 \uC2DC\uB3C4\uD558\uC138\uC694.");
433
+ return;
434
+ }
435
+ const id = newId.trim();
436
+ if (!isValidPageId(id)) {
437
+ setCreateError("id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7-\xB7_ \uC138\uADF8\uBA3C\uD2B8\uB97C /\uB85C \uC5F0\uACB0\uD55C \uD615\uD0DC\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC608: sales/orders)");
438
+ return;
439
+ }
440
+ if (pages.some((p) => p.id === id)) {
441
+ setCreateError("\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 id\uC785\uB2C8\uB2E4");
442
+ return;
443
+ }
444
+ setCreating(true);
445
+ setCreateError(null);
446
+ try {
447
+ const spec = TEMPLATES.find((t) => t.key === template)?.spec ?? TEMPLATES[0].spec;
448
+ await store.write(id, spec, { ifAbsent: true });
449
+ setCreateOpen(false);
450
+ setNewId("");
451
+ onEdit(id);
452
+ } catch (err) {
453
+ setCreateError(err instanceof PageExistsError ? "\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 id\uC785\uB2C8\uB2E4" : messageOf(err, "\uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4"));
454
+ } finally {
455
+ setCreating(false);
456
+ }
457
+ };
458
+ const duplicatePage = async (source) => {
459
+ try {
460
+ const record = await store.read(source.id);
461
+ if (!record) throw new Error("\uC6D0\uBCF8 \uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
462
+ let n = 1;
463
+ for (; ; ) {
464
+ const candidate = n === 1 ? `${source.id}-copy` : `${source.id}-copy${n}`;
465
+ try {
466
+ await store.write(candidate, record.spec, { ifAbsent: true });
467
+ break;
468
+ } catch (err) {
469
+ if (err instanceof PageExistsError && n < 50) {
470
+ n += 1;
471
+ continue;
472
+ }
473
+ throw err;
474
+ }
475
+ }
476
+ load();
477
+ } catch (err) {
478
+ setError(messageOf(err, "\uBCF5\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4"));
479
+ }
480
+ };
481
+ const removePage = async (page) => {
482
+ if (!window.confirm(`"${page.id}" \uD398\uC774\uC9C0\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?
483
+ \uC0AD\uC81C\uD558\uBA74 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`)) return;
484
+ try {
485
+ if (!await store.remove(page.id)) {
486
+ setError("\uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
487
+ return;
488
+ }
489
+ load();
490
+ } catch (err) {
491
+ setError(err instanceof StoreUnsupportedError ? err.message : messageOf(err, "\uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4"));
492
+ }
493
+ };
494
+ return /* @__PURE__ */ jsxs("div", { className: "sdui-editor-page wp-app", children: [
495
+ /* @__PURE__ */ jsxs("div", { className: "wp-page-head", children: [
496
+ /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
497
+ /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: "\uD398\uC774\uC9C0" }),
498
+ /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: "SDUI \uD398\uC774\uC9C0 \uC2A4\uD399\uC744 \uCD94\uAC00\xB7\uC218\uC815\uD569\uB2C8\uB2E4" })
499
+ ] }),
500
+ /* @__PURE__ */ jsx("div", { className: "wp-page-head__actions", children: /* @__PURE__ */ jsx(Button, { variant: "primary", icon: "plus", onClick: () => {
501
+ setCreateOpen(true);
502
+ setCreateError(null);
503
+ }, children: "\uC0C8 \uD398\uC774\uC9C0" }) })
504
+ ] }),
505
+ error ? /* @__PURE__ */ jsx(
506
+ Alert,
507
+ {
508
+ tone: "danger",
509
+ actions: listFailed ? /* @__PURE__ */ jsx(Button, { size: "xs", icon: "refresh-cw", onClick: load, children: "\uB2E4\uC2DC \uC2DC\uB3C4" }) : void 0,
510
+ onClose: () => setError(null),
511
+ children: error
512
+ }
513
+ ) : null,
514
+ caps.limitations.length > 0 ? /* @__PURE__ */ jsx(Alert, { tone: "info", title: `${caps.label} \uC800\uC7A5\uC18C\uB97C \uD3B8\uC9D1 \uC911\uC785\uB2C8\uB2E4`, children: /* @__PURE__ */ jsx("ul", { className: "sdui-editor-limitations", children: caps.limitations.map((limitation) => /* @__PURE__ */ jsx("li", { children: limitation }, limitation)) }) }) : null,
515
+ /* @__PURE__ */ jsxs(Card, { padding: "none", children: [
516
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-list-toolbar", children: [
517
+ /* @__PURE__ */ jsx(Input, { icon: "search", placeholder: "id \uB610\uB294 \uC81C\uBAA9 \uAC80\uC0C9", value: query, onChange: (e) => setQuery(e.target.value) }),
518
+ /* @__PURE__ */ jsxs(Badge, { tone: "brand", children: [
519
+ filtered?.length ?? 0,
520
+ "\uAC1C"
521
+ ] })
522
+ ] }),
523
+ listFailed ? (
524
+ // 목록을 못 받았으면 영원한 스피너 대신 상태를 분명히 밝힌다
525
+ /* @__PURE__ */ jsx(EmptyState, { title: "\uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4", actions: /* @__PURE__ */ jsx(Button, { icon: "refresh-cw", onClick: load, children: "\uB2E4\uC2DC \uC2DC\uB3C4" }), children: "\uC11C\uBC84 \uC5F0\uACB0\uACFC \uB85C\uADF8\uC778 \uC0C1\uD0DC\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694." })
526
+ ) : !filtered ? /* @__PURE__ */ jsx("div", { className: "sdui-editor-center", style: { padding: 48 }, children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: "\uBD88\uB7EC\uC624\uB294 \uC911" }) }) : /* @__PURE__ */ jsx(
527
+ Table,
528
+ {
529
+ columns: [
530
+ { key: "id", header: "ID", mono: true, width: "220px" },
531
+ {
532
+ key: "title",
533
+ header: "\uC81C\uBAA9",
534
+ strong: true,
535
+ // 손상된 스펙은 저장소가 corrupt로 표시해 내려준다 — 열어서 고칠 수 있게 눈에 띄게 알린다
536
+ render: (row) => row.corrupt ? /* @__PURE__ */ jsx(Badge, { tone: "danger", children: "JSON \uC190\uC0C1 \u2014 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uBCF5\uAD6C" }) : row.title ?? /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "(\uC81C\uBAA9 \uC5C6\uC74C)" })
537
+ },
538
+ { key: "updatedAt", header: "\uC218\uC815", width: "180px", render: (row) => formatDate(row.updatedAt) },
539
+ {
540
+ key: "_actions",
541
+ header: "",
542
+ width: "150px",
543
+ render: (row) => /* @__PURE__ */ jsxs("div", { className: "wp-row", style: { justifyContent: "flex-end", gap: 2 }, onClick: (e) => e.stopPropagation(), children: [
544
+ /* @__PURE__ */ jsx(IconButton, { icon: "pencil", label: "\uD3B8\uC9D1", size: "sm", onClick: () => onEdit(row.id) }),
545
+ /* @__PURE__ */ jsx(IconButton, { icon: "eye", label: "\uBCF4\uAE30", size: "sm", onClick: () => onPreview(row.id) }),
546
+ /* @__PURE__ */ jsx(IconButton, { icon: "copy", label: "\uBCF5\uC81C", size: "sm", onClick: () => void duplicatePage(row) }),
547
+ caps.remove ? /* @__PURE__ */ jsx(IconButton, { icon: "trash-2", label: "\uC0AD\uC81C", size: "sm", tone: "danger", onClick: () => void removePage(row) }) : null
548
+ ] })
549
+ }
550
+ ],
551
+ rows: filtered,
552
+ onRowClick: (row) => onEdit(row.id),
553
+ empty: /* @__PURE__ */ jsx(
554
+ EmptyState,
555
+ {
556
+ title: query ? "\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4" : "\uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
557
+ actions: query ? void 0 : /* @__PURE__ */ jsx(Button, { variant: "primary", icon: "plus", onClick: () => setCreateOpen(true), children: "\uCCAB \uD398\uC774\uC9C0 \uB9CC\uB4E4\uAE30" }),
558
+ children: query ? "\uB2E4\uB978 \uAC80\uC0C9\uC5B4\uB85C \uC2DC\uB3C4\uD574 \uBCF4\uC138\uC694." : "\uC0C8 \uD398\uC774\uC9C0\uB97C \uB9CC\uB4E4\uC5B4 \uC2DC\uC791\uD558\uC138\uC694."
559
+ }
560
+ )
561
+ }
562
+ )
563
+ ] }),
564
+ createOpen ? /* @__PURE__ */ jsxs(
565
+ SduiDialog,
566
+ {
567
+ title: "\uC0C8 \uD398\uC774\uC9C0",
568
+ subtitle: "id\uB294 URL \uACBD\uB85C\uAC00 \uB429\uB2C8\uB2E4",
569
+ icon: "plus",
570
+ onClose: () => setCreateOpen(false),
571
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
572
+ /* @__PURE__ */ jsx(Button, { onClick: () => setCreateOpen(false), children: "\uCDE8\uC18C" }),
573
+ /* @__PURE__ */ jsx(Button, { variant: "primary", loading: creating, disabled: pages === null, onClick: () => void createPage(), children: "\uB9CC\uB4E4\uAE30" })
574
+ ] }),
575
+ children: [
576
+ pages === null ? /* @__PURE__ */ jsx(Alert, { tone: "warning", title: "\uC9C0\uAE08\uC740 \uC0C8 \uD398\uC774\uC9C0\uB97C \uB9CC\uB4E4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4", children: "\uD398\uC774\uC9C0 \uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD574 \uC5B4\uB5A4 id\uAC00 \uC774\uBBF8 \uC4F0\uC774\uB294\uC9C0 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBAA9\uB85D\uC744 \uBA3C\uC800 \uB2E4\uC2DC \uBD88\uB7EC\uC624\uC138\uC694." }) : null,
577
+ createError ? /* @__PURE__ */ jsx(Alert, { tone: "danger", children: createError }) : null,
578
+ /* @__PURE__ */ jsx(Field, { label: "\uD398\uC774\uC9C0 id", required: true, hint: "\uC608: home, sales/orders", children: /* @__PURE__ */ jsx(
579
+ Input,
580
+ {
581
+ mono: true,
582
+ value: newId,
583
+ autoFocus: true,
584
+ disabled: pages === null,
585
+ placeholder: "sales/orders",
586
+ onChange: (e) => setNewId(e.target.value),
587
+ onKeyDown: (e) => {
588
+ if (e.key === "Enter") {
589
+ e.preventDefault();
590
+ if (pages !== null) void createPage();
591
+ }
592
+ }
593
+ }
594
+ ) }),
595
+ /* @__PURE__ */ jsx(Field, { label: "\uD15C\uD50C\uB9BF", children: /* @__PURE__ */ jsx(
596
+ Select,
597
+ {
598
+ value: template,
599
+ onChange: (e) => setTemplate(e.target.value),
600
+ options: TEMPLATES.map((t) => ({ value: t.key, label: `${t.label} \u2014 ${t.description}` }))
601
+ }
602
+ ) })
603
+ ]
604
+ }
605
+ ) : null
606
+ ] });
607
+ }
608
+ var KNOWN_COMPONENTS = new Set(Object.keys(defaultRegistry));
609
+ var STRUCTURAL_ISSUES = /* @__PURE__ */ new Set([
610
+ "\uC2A4\uD399\uC774 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4",
611
+ "version\uC740 1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4",
612
+ "body\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4",
613
+ "children \uD56D\uBAA9\uC740 \uBB38\uC790\uC5F4 \uB610\uB294 \uB178\uB4DC \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4",
614
+ "component\uB294 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC740 \uBB38\uC790\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4",
615
+ "props\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4",
616
+ "slots\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4",
617
+ "\uC2AC\uB86F \uAC12\uC740 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4",
618
+ "children\uC740 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4"
619
+ ]);
620
+ function gradeIssues(spec) {
621
+ return validateSpec(spec, KNOWN_COMPONENTS).map((issue) => ({
622
+ ...issue,
623
+ severity: STRUCTURAL_ISSUES.has(issue.message) ? "error" : "warning"
624
+ }));
625
+ }
626
+ var editorTheme = EditorView.theme({
627
+ "&": { height: "100%", fontSize: "12.5px" },
628
+ ".cm-scroller": {
629
+ fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace",
630
+ lineHeight: "1.55"
631
+ },
632
+ ".cm-gutters": { background: "var(--wp-slate-25)", borderRight: "1px solid var(--wp-slate-200)", color: "var(--wp-slate-400)" },
633
+ "&.cm-focused": { outline: "none" },
634
+ ".cm-activeLine": { background: "rgba(10, 115, 201, 0.05)" },
635
+ ".cm-activeLineGutter": { background: "rgba(10, 115, 201, 0.08)" }
636
+ });
637
+ function JsonEditor({ value, onChange, onSave }) {
638
+ const hostRef = useRef(null);
639
+ const viewRef = useRef(null);
640
+ const onChangeRef = useRef(onChange);
641
+ onChangeRef.current = onChange;
642
+ const onSaveRef = useRef(onSave);
643
+ onSaveRef.current = onSave;
644
+ useEffect(() => {
645
+ if (!hostRef.current) return;
646
+ const state = EditorState.create({
647
+ doc: value,
648
+ extensions: [
649
+ basicSetup,
650
+ json(),
651
+ editorTheme,
652
+ keymap.of([
653
+ {
654
+ key: "Mod-s",
655
+ run: () => {
656
+ onSaveRef.current?.();
657
+ return true;
658
+ }
659
+ },
660
+ indentWithTab
661
+ ]),
662
+ EditorView.updateListener.of((update) => {
663
+ if (update.docChanged) onChangeRef.current(update.state.doc.toString());
664
+ })
665
+ ]
666
+ });
667
+ const view = new EditorView({ state, parent: hostRef.current });
668
+ viewRef.current = view;
669
+ hostRef.current.cmView = view;
670
+ return () => {
671
+ view.destroy();
672
+ viewRef.current = null;
673
+ };
674
+ }, []);
675
+ useEffect(() => {
676
+ const view = viewRef.current;
677
+ if (!view) return;
678
+ const current = view.state.doc.toString();
679
+ if (current !== value) {
680
+ view.dispatch({ changes: { from: 0, to: current.length, insert: value } });
681
+ }
682
+ }, [value]);
683
+ return /* @__PURE__ */ jsx("div", { ref: hostRef, className: "sdui-editor-json" });
684
+ }
685
+ var SPECIAL_VALUES = [
686
+ ['{ "$state": "\uD0A4", "default": \u2026 }', "\uD398\uC774\uC9C0 \uC0C1\uD0DC \uBC14\uC778\uB529 (\uACBD\uB85C \uC870\uD68C \uC9C0\uC6D0: form.$errors.name)"],
687
+ ['{ "$item": "\uD544\uB4DC" } / { "$item": true }', "ForEach \uD604\uC7AC \uD589\uC758 \uD544\uB4DC / \uD589 \uC804\uCCB4"],
688
+ ['{ "$index": true }', "ForEach \uD604\uC7AC \uC778\uB371\uC2A4 (0\uBD80\uD130)"],
689
+ ['{ "$calc": { "op": "\u2026", "args": [\u2026] } }', "\uACC4\uC0B0\uAC12 \u2014 sum\xB7sub\xB7mul\xB7div\xB7percent\xB7count\xB7concat\xB7encode\xB7eq\xB7gt\xB7gte\xB7lt\xB7lte\xB7not"]
690
+ ];
691
+ var ACTIONS = [
692
+ ["navigate", "to \u2014 \uD398\uC774\uC9C0 \uC774\uB3D9"],
693
+ ["setState", "key, value \uB610\uB294 fromEvent:true(\uC774\uBCA4\uD2B8 \uAC12 \uC800\uC7A5)"],
694
+ ["open / close", "key \u2014 \uC0C1\uD0DC\uB97C true/false\uB85C (Show+Dialog \uC870\uD569)"],
695
+ ["toast", "tone\xB7title\xB7message \u2014 \uC6B0\uD558\uB2E8 \uC54C\uB9BC 4\uCD08"],
696
+ ["submitForm", "form, endpoint, method?, onSuccess?, onError? \u2014 \uAC80\uC99D \uD6C4 \uC804\uC1A1"],
697
+ ["arrayAppend/Remove/Move/Update", "key(+value/index/from\xB7to/field) \u2014 \uBC18\uBCF5 \uD589 \uD3B8\uC9D1"],
698
+ ["download", "endpoint, filename? \u2014 \uD30C\uC77C \uC800\uC7A5"],
699
+ ["refresh / invalidate", "\uC5B4\uB311\uD130 \uC704\uC784 \u2014 \uC7AC\uC870\uD68C"],
700
+ ["print", "\uBE0C\uB77C\uC6B0\uC800 \uC778\uC1C4"],
701
+ ["custom", "name, payload? \u2014 \uD638\uC2A4\uD2B8 \uC571 \uD655\uC7A5 \uC9C0\uC810"]
702
+ ];
703
+ var RULES = "required \xB7 min \xB7 max \xB7 minLength \xB7 maxLength \xB7 pattern \xB7 notBeforeField \xB7 custom";
704
+ function HelpPanel({ onClose }) {
705
+ const components = Object.keys(defaultRegistry).sort();
706
+ return /* @__PURE__ */ jsxs("aside", { className: "sdui-editor-help", children: [
707
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-help__head", children: [
708
+ /* @__PURE__ */ jsx("span", { className: "wp-h5", children: "\uC2A4\uD399 \uC791\uC131 \uB3C4\uC6C0\uB9D0" }),
709
+ /* @__PURE__ */ jsx(IconButton, { icon: "x", label: "\uB2EB\uAE30", size: "sm", onClick: onClose })
710
+ ] }),
711
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-help__body", children: [
712
+ /* @__PURE__ */ jsxs("section", { children: [
713
+ /* @__PURE__ */ jsx("h3", { className: "wp-overline", children: "\uD398\uC774\uC9C0 \uAD6C\uC870" }),
714
+ /* @__PURE__ */ jsx("pre", { className: "sdui-editor-help__code", children: `{
715
+ "version": 1,
716
+ "title": "\uC81C\uBAA9",
717
+ "subtitle": "\uBD80\uC81C",
718
+ "urlState": ["tab"],
719
+ "body": [ /* \uB178\uB4DC \uBC30\uC5F4 */ ]
720
+ }` }),
721
+ /* @__PURE__ */ jsx("pre", { className: "sdui-editor-help__code", children: `{
722
+ "component": "Button",
723
+ "props": { "variant": "primary",
724
+ "onClick": { "action": "\u2026" } },
725
+ "slots": { "footer": [ /* \uB178\uB4DC */ ] },
726
+ "children": [ "\uD14D\uC2A4\uD2B8 \uB610\uB294 \uB178\uB4DC" ]
727
+ }` })
728
+ ] }),
729
+ /* @__PURE__ */ jsxs("section", { children: [
730
+ /* @__PURE__ */ jsx("h3", { className: "wp-overline", children: "\uD2B9\uC218 \uAC12 (props \uC790\uB9AC)" }),
731
+ /* @__PURE__ */ jsx("dl", { className: "sdui-editor-help__dl", children: SPECIAL_VALUES.map(([code, desc]) => /* @__PURE__ */ jsxs("div", { children: [
732
+ /* @__PURE__ */ jsx("dt", { children: /* @__PURE__ */ jsx("code", { children: code }) }),
733
+ /* @__PURE__ */ jsx("dd", { children: desc })
734
+ ] }, code)) })
735
+ ] }),
736
+ /* @__PURE__ */ jsxs("section", { children: [
737
+ /* @__PURE__ */ jsx("h3", { className: "wp-overline", children: "\uC561\uC158 (on* \uD504\uB86D \uC790\uB9AC, \uBC30\uC5F4\uC774\uBA74 \uC21C\uCC28 \uC2E4\uD589)" }),
738
+ /* @__PURE__ */ jsx("dl", { className: "sdui-editor-help__dl", children: ACTIONS.map(([name, desc]) => /* @__PURE__ */ jsxs("div", { children: [
739
+ /* @__PURE__ */ jsx("dt", { children: /* @__PURE__ */ jsx("code", { children: name }) }),
740
+ /* @__PURE__ */ jsx("dd", { children: desc })
741
+ ] }, name)) })
742
+ ] }),
743
+ /* @__PURE__ */ jsxs("section", { children: [
744
+ /* @__PURE__ */ jsx("h3", { className: "wp-overline", children: "\uD3FC \uAC80\uC99D \uADDC\uCE59" }),
745
+ /* @__PURE__ */ jsx("p", { className: "wp-body-sm", children: RULES }),
746
+ /* @__PURE__ */ jsxs("p", { className: "wp-caption", children: [
747
+ "\uC5D0\uB7EC \uD45C\uC2DC: Field.error\uC5D0 ",
748
+ '{ "$state": "\uD3FCid.$errors.\uD544\uB4DC" }',
749
+ " \uBC14\uC778\uB529 \xB7 \uC81C\uCD9C \uBC84\uD2BC loading\uC5D0 ",
750
+ '{ "$state": "\uD3FCid.$submitting" }'
751
+ ] })
752
+ ] }),
753
+ /* @__PURE__ */ jsxs("section", { children: [
754
+ /* @__PURE__ */ jsxs("h3", { className: "wp-overline", children: [
755
+ "\uCEF4\uD3EC\uB10C\uD2B8 \uC5B4\uD718 (",
756
+ components.length + 1,
757
+ "\uC885)"
758
+ ] }),
759
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-help__chips", children: [
760
+ /* @__PURE__ */ jsx("code", { className: "sdui-editor-help__chip", children: "ForEach" }),
761
+ components.map((name) => /* @__PURE__ */ jsx("code", { className: "sdui-editor-help__chip", children: name }, name))
762
+ ] }),
763
+ /* @__PURE__ */ jsx("p", { className: "wp-caption", children: "ForEach\uB294 \uB80C\uB354\uB7EC \uB0B4\uC7A5 \u2014 props.source(\uBC30\uC5F4) + children \uBC18\uBCF5, slots.empty." })
764
+ ] })
765
+ ] })
766
+ ] });
767
+ }
768
+ var PreviewBoundary = class extends React.Component {
769
+ state = { error: null };
770
+ static getDerivedStateFromError(error) {
771
+ return { error };
772
+ }
773
+ componentDidUpdate(prev) {
774
+ if (this.state.error && (prev.resetKey !== this.props.resetKey || prev.signature !== this.props.signature)) {
775
+ this.setState({ error: null });
776
+ }
777
+ }
778
+ render() {
779
+ if (this.state.error) {
780
+ return /* @__PURE__ */ jsx(Alert, { tone: "danger", title: "\uBBF8\uB9AC\uBCF4\uAE30 \uB80C\uB354 \uC624\uB958", children: this.state.error.message });
781
+ }
782
+ return this.props.children;
783
+ }
784
+ };
785
+ var UNKNOWN_BASE = "__unknown-base__";
786
+ function readDraft(key) {
787
+ const raw = sessionStorage.getItem(key);
788
+ if (raw === null) return null;
789
+ try {
790
+ const parsed = JSON.parse(raw);
791
+ if (parsed && parsed.v === 1 && typeof parsed.text === "string") {
792
+ return { v: 1, base: typeof parsed.base === "string" ? parsed.base : null, text: parsed.text };
793
+ }
794
+ } catch {
795
+ }
796
+ return { v: 1, base: null, text: raw };
797
+ }
798
+ function writeDraft(key, base, text) {
799
+ sessionStorage.setItem(key, JSON.stringify({ v: 1, base, text }));
800
+ }
801
+ function EditorView2({ store, id, onBack, onPreview }) {
802
+ const caps = store.capabilities;
803
+ const draftKey = `sdui-draft:${id}`;
804
+ const [loaded, setLoaded] = useState(null);
805
+ const [serverVersion, setServerVersion] = useState(null);
806
+ const [baseVersion, setBaseVersion] = useState(null);
807
+ const [text, setText] = useState("");
808
+ const textRef = useRef(text);
809
+ textRef.current = text;
810
+ const [loadError, setLoadError] = useState(null);
811
+ const [saving, setSaving] = useState(false);
812
+ const [savedAt, setSavedAt] = useState(null);
813
+ const [saveError, setSaveError] = useState(null);
814
+ const [conflict, setConflict] = useState(false);
815
+ const [gone, setGone] = useState(false);
816
+ const [corrupt, setCorrupt] = useState(false);
817
+ const [draftRestored, setDraftRestored] = useState(false);
818
+ const [helpOpen, setHelpOpen] = useState(false);
819
+ const [previewKey, setPreviewKey] = useState(0);
820
+ const [previewNotice, setPreviewNotice] = useState(null);
821
+ const noticeTimer = useRef(null);
822
+ useEffect(() => {
823
+ if (!id) return;
824
+ setLoaded(null);
825
+ setLoadError(null);
826
+ setConflict(false);
827
+ setGone(false);
828
+ let alive = true;
829
+ store.read(id).then((record) => {
830
+ if (!alive) return;
831
+ if (!record) {
832
+ setLoadError("\uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
833
+ return;
834
+ }
835
+ const pretty = JSON.stringify(record.spec, null, 2);
836
+ setLoaded(pretty);
837
+ setServerVersion(record.version);
838
+ setCorrupt(record.corrupt === true);
839
+ const draft = readDraft(`sdui-draft:${id}`);
840
+ if (draft && draft.text !== pretty) {
841
+ setText(draft.text);
842
+ setDraftRestored(true);
843
+ setBaseVersion(draft.base ?? UNKNOWN_BASE);
844
+ } else {
845
+ setText(pretty);
846
+ setDraftRestored(false);
847
+ setBaseVersion(record.version);
848
+ }
849
+ }).catch((err) => {
850
+ if (alive) setLoadError(messageOf(err, "\uD398\uC774\uC9C0\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"));
851
+ });
852
+ return () => {
853
+ alive = false;
854
+ };
855
+ }, [id, store]);
856
+ useEffect(() => () => {
857
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
858
+ }, []);
859
+ const [debounced, setDebounced] = useState("");
860
+ useEffect(() => {
861
+ const timer = setTimeout(() => setDebounced(text), 300);
862
+ return () => clearTimeout(timer);
863
+ }, [text]);
864
+ useEffect(() => {
865
+ if (loaded === null || debounced !== textRef.current) return;
866
+ if (debounced && debounced !== loaded) writeDraft(draftKey, baseVersion, debounced);
867
+ else sessionStorage.removeItem(draftKey);
868
+ }, [debounced, loaded, draftKey, baseVersion]);
869
+ const parsed = useMemo(() => {
870
+ if (!debounced.trim()) return { spec: null, error: null };
871
+ try {
872
+ return { spec: JSON.parse(debounced), error: null };
873
+ } catch (err) {
874
+ return { spec: null, error: err instanceof Error ? err.message : "JSON \uD30C\uC2F1 \uC624\uB958" };
875
+ }
876
+ }, [debounced]);
877
+ const issues = useMemo(() => parsed.spec ? gradeIssues(parsed.spec) : [], [parsed.spec]);
878
+ const errorCount = issues.filter((issue) => issue.severity === "error").length;
879
+ const dirty = loaded !== null && text !== loaded;
880
+ const draftStale = draftRestored && baseVersion !== null && baseVersion !== serverVersion;
881
+ const save = async (force = false) => {
882
+ if (saving) return;
883
+ const snapshot = text;
884
+ let spec;
885
+ try {
886
+ spec = JSON.parse(snapshot);
887
+ } catch {
888
+ setSaveError("JSON\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
889
+ return;
890
+ }
891
+ const structural = gradeIssues(spec).filter((issue) => issue.severity === "error");
892
+ if (structural.length > 0) {
893
+ setConflict(false);
894
+ setGone(false);
895
+ setSaveError(
896
+ `\uC2A4\uD399 \uAD6C\uC870 \uC624\uB958 ${structural.length}\uAC74 \u2014 \uC800\uC7A5\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4: ${structural[0].path} ${structural[0].message}`
897
+ );
898
+ return;
899
+ }
900
+ setSaving(true);
901
+ setSaveError(null);
902
+ try {
903
+ const saved = await store.write(
904
+ id,
905
+ spec,
906
+ // force면 기대 버전을 빼 업서트한다(사용자가 '무시하고 덮어쓰기'를 골랐을 때)
907
+ force || baseVersion === null ? {} : { expect: { kind: "version", value: baseVersion } }
908
+ );
909
+ const pretty = JSON.stringify(spec, null, 2);
910
+ setLoaded(pretty);
911
+ setServerVersion(saved.version);
912
+ setBaseVersion(saved.version);
913
+ setConflict(false);
914
+ setGone(false);
915
+ setCorrupt(false);
916
+ setDraftRestored(false);
917
+ sessionStorage.removeItem(draftKey);
918
+ setText((current) => current === snapshot ? pretty : current);
919
+ setSavedAt((/* @__PURE__ */ new Date()).toLocaleTimeString("ko-KR"));
920
+ } catch (err) {
921
+ if (err instanceof VersionMismatchError) {
922
+ setConflict(true);
923
+ setServerVersion(err.current.version);
924
+ setSaveError(err.message);
925
+ } else if (err instanceof PageGoneError) {
926
+ setGone(true);
927
+ setSaveError(err.message);
928
+ } else {
929
+ setSaveError(messageOf(err, "\uC800\uC7A5\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4"));
930
+ }
931
+ } finally {
932
+ setSaving(false);
933
+ }
934
+ };
935
+ const recreate = async () => {
936
+ if (saving) return;
937
+ const snapshot = text;
938
+ let spec;
939
+ try {
940
+ spec = JSON.parse(snapshot);
941
+ } catch {
942
+ setSaveError("JSON\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
943
+ return;
944
+ }
945
+ setSaving(true);
946
+ try {
947
+ const saved = await store.write(id, spec, { ifAbsent: true });
948
+ const pretty = JSON.stringify(spec, null, 2);
949
+ setLoaded(pretty);
950
+ setServerVersion(saved.version);
951
+ setBaseVersion(saved.version);
952
+ setGone(false);
953
+ setCorrupt(false);
954
+ setSaveError(null);
955
+ setDraftRestored(false);
956
+ sessionStorage.removeItem(draftKey);
957
+ setText((current) => current === snapshot ? pretty : current);
958
+ setSavedAt((/* @__PURE__ */ new Date()).toLocaleTimeString("ko-KR"));
959
+ } catch (err) {
960
+ setSaveError(messageOf(err, "\uC0C8\uB85C \uB9CC\uB4E4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"));
961
+ } finally {
962
+ setSaving(false);
963
+ }
964
+ };
965
+ const reloadFromServer = async () => {
966
+ try {
967
+ const record = await store.read(id);
968
+ if (!record) {
969
+ setSaveError("\uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
970
+ return;
971
+ }
972
+ const pretty = JSON.stringify(record.spec, null, 2);
973
+ setLoaded(pretty);
974
+ setServerVersion(record.version);
975
+ setBaseVersion(record.version);
976
+ setText(pretty);
977
+ setConflict(false);
978
+ setGone(false);
979
+ setCorrupt(record.corrupt === true);
980
+ setSaveError(null);
981
+ setDraftRestored(false);
982
+ sessionStorage.removeItem(draftKey);
983
+ } catch (err) {
984
+ setSaveError(messageOf(err, "\uB2E4\uC2DC \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"));
985
+ }
986
+ };
987
+ const format = () => {
988
+ try {
989
+ setText(JSON.stringify(JSON.parse(text), null, 2));
990
+ } catch {
991
+ setSaveError("JSON\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544 \uC815\uB82C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
992
+ }
993
+ };
994
+ const remove = async () => {
995
+ if (!window.confirm(`"${id}" \uD398\uC774\uC9C0\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?
996
+ \uC0AD\uC81C\uD558\uBA74 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`)) return;
997
+ try {
998
+ await store.remove(id);
999
+ sessionStorage.removeItem(draftKey);
1000
+ onBack();
1001
+ } catch (err) {
1002
+ setSaveError(err instanceof StoreUnsupportedError ? err.message : messageOf(err, "\uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4"));
1003
+ }
1004
+ };
1005
+ useEffect(() => {
1006
+ if (!dirty) return;
1007
+ const handler = (event) => {
1008
+ event.preventDefault();
1009
+ event.returnValue = "";
1010
+ };
1011
+ window.addEventListener("beforeunload", handler);
1012
+ return () => window.removeEventListener("beforeunload", handler);
1013
+ }, [dirty]);
1014
+ const notifyPreview = useCallback((what) => {
1015
+ setPreviewNotice(`${what} \u2014 \uBBF8\uB9AC\uBCF4\uAE30\uC5D0\uC11C\uB294 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
1016
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
1017
+ noticeTimer.current = setTimeout(() => setPreviewNotice(null), 2500);
1018
+ }, []);
1019
+ const previewAdapter = useMemo(
1020
+ () => ({
1021
+ navigate: (to) => notifyPreview(`navigate \u2192 ${to}`),
1022
+ // 요청은 보내지 않되 성공(null)으로 응답해 onSuccess 흐름은 그려 볼 수 있게 한다
1023
+ fetch: async (path, init) => {
1024
+ notifyPreview(`${init.method} ${path}`);
1025
+ return null;
1026
+ },
1027
+ // download는 rawFetch 결과를 파일로 내려받는다 — 실패로 끊어 빈 파일이 저장되는 것도 막는다
1028
+ rawFetch: async (path) => {
1029
+ notifyPreview(`download ${path}`);
1030
+ throw new Error("\uBBF8\uB9AC\uBCF4\uAE30\uC5D0\uC11C\uB294 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
1031
+ },
1032
+ invalidate: (prefix) => notifyPreview(`invalidate ${prefix}`),
1033
+ refresh: () => notifyPreview("refresh")
1034
+ }),
1035
+ [notifyPreview]
1036
+ );
1037
+ if (!id) {
1038
+ return /* @__PURE__ */ jsxs("div", { className: "sdui-editor-page wp-app", children: [
1039
+ /* @__PURE__ */ jsx(Alert, { tone: "warning", title: "\uD398\uC774\uC9C0 id\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4", children: "\uD3B8\uC9D1\uD560 \uD398\uC774\uC9C0\uB97C \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD\uD558\uC138\uC694." }),
1040
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { icon: "arrow-left", onClick: onBack, children: "\uBAA9\uB85D\uC73C\uB85C" }) })
1041
+ ] });
1042
+ }
1043
+ if (loadError) {
1044
+ return /* @__PURE__ */ jsxs("div", { className: "sdui-editor-page wp-app", children: [
1045
+ /* @__PURE__ */ jsx(Alert, { tone: "danger", title: "\uBD88\uB7EC\uC624\uAE30 \uC2E4\uD328", children: loadError }),
1046
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { icon: "arrow-left", onClick: onBack, children: "\uBAA9\uB85D\uC73C\uB85C" }) })
1047
+ ] });
1048
+ }
1049
+ if (loaded === null) {
1050
+ return /* @__PURE__ */ jsx("div", { className: "sdui-editor-center", children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: "\uD398\uC774\uC9C0 \uBD88\uB7EC\uC624\uB294 \uC911" }) });
1051
+ }
1052
+ return /* @__PURE__ */ jsxs("div", { className: "sdui-editor-layout wp-app", children: [
1053
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-toolbar", children: [
1054
+ /* @__PURE__ */ jsx(IconButton, { icon: "arrow-left", label: "\uBAA9\uB85D\uC73C\uB85C", onClick: onBack }),
1055
+ /* @__PURE__ */ jsx("span", { className: "wp-mono sdui-editor-toolbar__id", children: id }),
1056
+ dirty ? /* @__PURE__ */ jsx(Badge, { tone: "warning", children: "\uC800\uC7A5 \uC548 \uB428" }) : savedAt ? /* @__PURE__ */ jsxs(Badge, { tone: "success", children: [
1057
+ "\uC800\uC7A5\uB428 ",
1058
+ savedAt
1059
+ ] }) : null,
1060
+ parsed.error ? /* @__PURE__ */ jsx(Badge, { tone: "danger", children: "JSON \uC624\uB958" }) : errorCount > 0 ? /* @__PURE__ */ jsxs(Badge, { tone: "danger", children: [
1061
+ "\uAD6C\uC870 \uC624\uB958 ",
1062
+ errorCount
1063
+ ] }) : issues.length > 0 ? /* @__PURE__ */ jsxs(Badge, { tone: "warning", children: [
1064
+ "\uACBD\uACE0 ",
1065
+ issues.length
1066
+ ] }) : null,
1067
+ /* @__PURE__ */ jsx("span", { className: "wp-spacer" }),
1068
+ /* @__PURE__ */ jsx(Button, { size: "sm", icon: "code", onClick: format, children: "\uC815\uB82C" }),
1069
+ /* @__PURE__ */ jsx(Button, { size: "sm", icon: "refresh-cw", onClick: () => setPreviewKey((k) => k + 1), children: "\uBBF8\uB9AC\uBCF4\uAE30 \uCD08\uAE30\uD654" }),
1070
+ /* @__PURE__ */ jsx(Button, { size: "sm", icon: "eye", onClick: () => onPreview(id), children: "\uBCF4\uAE30" }),
1071
+ /* @__PURE__ */ jsx(Button, { size: "sm", icon: "circle-help", onClick: () => setHelpOpen((v) => !v), children: "\uB3C4\uC6C0\uB9D0" }),
1072
+ caps.remove ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "danger", icon: "trash-2", onClick: () => void remove(), children: "\uC0AD\uC81C" }) : null,
1073
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "primary", icon: "save", loading: saving, disabled: !dirty, onClick: () => void save(), children: "\uC800\uC7A5 (\u2318S)" })
1074
+ ] }),
1075
+ /* @__PURE__ */ jsx("div", { className: "sdui-editor-banner", children: /* @__PURE__ */ jsxs(Alert, { tone: "info", title: `${caps.label}\uC758 \uC2A4\uD399\uC744 \uC9C1\uC811 \uD3B8\uC9D1\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4`, children: [
1076
+ "\uC800\uC7A5\uD558\uBA74 \uADF8 \uC989\uC2DC \uC6B4\uC601 \uD654\uBA74\uC5D0 \uBC18\uC601\uB429\uB2C8\uB2E4(\uCD08\uC548 \uBCF4\uAD00 \uB2E8\uACC4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4). \uBBF8\uB9AC\uBCF4\uAE30\uB294 \uAD8C\uD55C \uC7AC\uB2E8",
1077
+ /* @__PURE__ */ jsx("b", { children: " \uC774\uC804" }),
1078
+ "\uC758 \uC6D0\uBCF8\uC774\uB77C, \uC2E4\uC81C \uC0AC\uC6A9\uC790\uC5D0\uAC8C\uB294 ",
1079
+ /* @__PURE__ */ jsx("code", { children: "Can" }),
1080
+ " \uC870\uAC74\uC5D0 \uB530\uB77C \uC77C\uBD80\uAC00 \uBCF4\uC774\uC9C0 \uC54A\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
1081
+ ] }) }),
1082
+ corrupt ? /* @__PURE__ */ jsx("div", { className: "sdui-editor-banner", children: /* @__PURE__ */ jsx(Alert, { tone: "warning", title: "\uC800\uC7A5\uB41C \uC2A4\uD399\uC774 \uC190\uC0C1\uB418\uC5B4 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4", children: "\uB0B4\uC6A9\uC744 \uBCF5\uAD6C\uD560 \uC218 \uC5C6\uC5B4 \uBE44\uC5B4 \uC788\uB294 \uC0C1\uD0DC\uB85C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4. \uC62C\uBC14\uB978 \uC2A4\uD399\uC744 \uC785\uB825\uD574 \uC800\uC7A5\uD558\uBA74 \uC774 \uD398\uC774\uC9C0\uAC00 \uB418\uC0B4\uC544\uB0A9\uB2C8\uB2E4." }) }) : null,
1083
+ draftRestored ? /* @__PURE__ */ jsx("div", { className: "sdui-editor-banner", children: /* @__PURE__ */ jsx(
1084
+ Alert,
1085
+ {
1086
+ tone: draftStale ? "warning" : "info",
1087
+ title: draftStale ? "\uCD08\uC548\uC744 \uBCF5\uC6D0\uD588\uC2B5\uB2C8\uB2E4 \u2014 \uADF8 \uC0AC\uC774 \uC11C\uBC84 \uB0B4\uC6A9\uC774 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4" : "\uC800\uC7A5\uB418\uC9C0 \uC54A\uC558\uB358 \uCD08\uC548\uC744 \uBCF5\uC6D0\uD588\uC2B5\uB2C8\uB2E4",
1088
+ actions: /* @__PURE__ */ jsx(Button, { size: "xs", onClick: () => void reloadFromServer(), children: "\uCD08\uC548 \uBC84\uB9AC\uACE0 \uC800\uC7A5\uBCF8 \uC5F4\uAE30" }),
1089
+ onClose: () => setDraftRestored(false),
1090
+ children: draftStale ? "\uC774 \uCD08\uC548\uC740 \uC9C0\uAE08 \uC11C\uBC84\uC5D0 \uC800\uC7A5\uB41C \uAC83\uBCF4\uB2E4 \uC774\uC804 \uBC84\uC804\uC744 \uAE30\uC900\uC73C\uB85C \uB9CC\uB4E4\uC5B4\uC84C\uC2B5\uB2C8\uB2E4. \uADF8\uB300\uB85C \uC800\uC7A5\uD558\uBA74 \uCDA9\uB3CC\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4." : null
1091
+ }
1092
+ ) }) : null,
1093
+ saveError ? /* @__PURE__ */ jsx("div", { className: "sdui-editor-banner", children: /* @__PURE__ */ jsx(
1094
+ Alert,
1095
+ {
1096
+ tone: conflict ? "warning" : "danger",
1097
+ title: conflict ? "\uC800\uC7A5 \uCDA9\uB3CC" : gone ? "\uD398\uC774\uC9C0\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" : void 0,
1098
+ actions: conflict ? /* @__PURE__ */ jsxs(Fragment, { children: [
1099
+ /* @__PURE__ */ jsx(Button, { size: "xs", onClick: () => void reloadFromServer(), children: "\uC11C\uBC84 \uB0B4\uC6A9 \uBD88\uB7EC\uC624\uAE30 (\uB0B4 \uC218\uC815 \uBC84\uB9BC)" }),
1100
+ /* @__PURE__ */ jsx(Button, { size: "xs", variant: "danger", onClick: () => void save(true), children: "\uBB34\uC2DC\uD558\uACE0 \uB36E\uC5B4\uC4F0\uAE30" })
1101
+ ] }) : gone ? /* @__PURE__ */ jsxs(Fragment, { children: [
1102
+ /* @__PURE__ */ jsx(Button, { size: "xs", variant: "primary", loading: saving, onClick: () => void recreate(), children: "\uC774 \uB0B4\uC6A9\uC73C\uB85C \uC0C8\uB85C \uB9CC\uB4E4\uAE30" }),
1103
+ /* @__PURE__ */ jsx(Button, { size: "xs", onClick: onBack, children: "\uBAA9\uB85D\uC73C\uB85C" })
1104
+ ] }) : void 0,
1105
+ onClose: () => {
1106
+ setSaveError(null);
1107
+ setConflict(false);
1108
+ setGone(false);
1109
+ },
1110
+ children: saveError
1111
+ }
1112
+ ) }) : null,
1113
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-split", children: [
1114
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-pane", children: [
1115
+ /* @__PURE__ */ jsx(JsonEditor, { value: text, onChange: setText, onSave: () => void save() }),
1116
+ parsed.error || issues.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "sdui-editor-issues", children: [
1117
+ parsed.error ? /* @__PURE__ */ jsxs("div", { className: "sdui-editor-issue sdui-editor-issue--error", children: [
1118
+ /* @__PURE__ */ jsx("span", { className: "wp-mono-xs", children: "JSON" }),
1119
+ " ",
1120
+ parsed.error
1121
+ ] }) : null,
1122
+ issues.slice(0, 20).map((issue, i) => /* @__PURE__ */ jsxs(
1123
+ "div",
1124
+ {
1125
+ className: `sdui-editor-issue${issue.severity === "error" ? " sdui-editor-issue--error" : ""}`,
1126
+ children: [
1127
+ /* @__PURE__ */ jsx("span", { className: "wp-mono-xs", children: issue.path }),
1128
+ " ",
1129
+ issue.message
1130
+ ]
1131
+ },
1132
+ i
1133
+ )),
1134
+ issues.length > 20 ? /* @__PURE__ */ jsxs("div", { className: "sdui-editor-issue", children: [
1135
+ "\u2026\uC678 ",
1136
+ issues.length - 20,
1137
+ "\uAC74"
1138
+ ] }) : null
1139
+ ] }) : null
1140
+ ] }),
1141
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-preview", children: [
1142
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-preview__head", children: [
1143
+ /* @__PURE__ */ jsx("span", { className: "wp-overline", children: "\uBBF8\uB9AC\uBCF4\uAE30" }),
1144
+ previewNotice ? /* @__PURE__ */ jsx("span", { className: "sdui-editor-preview__notice", children: previewNotice }) : null
1145
+ ] }),
1146
+ /* @__PURE__ */ jsx("div", { className: "sdui-editor-preview__body", children: parsed.spec ? /* @__PURE__ */ jsx(PreviewBoundary, { resetKey: previewKey, signature: debounced, children: /* @__PURE__ */ jsx(SduiRenderer, { page: parsed.spec, adapter: previewAdapter, syncUrlState: false }, previewKey) }) : /* @__PURE__ */ jsx(Alert, { tone: parsed.error ? "warning" : "neutral", children: parsed.error ? "JSON\uC744 \uACE0\uCE58\uBA74 \uBBF8\uB9AC\uBCF4\uAE30\uAC00 \uB2E4\uC2DC \uADF8\uB824\uC9D1\uB2C8\uB2E4." : "\uC2A4\uD399\uC744 \uC785\uB825\uD558\uC138\uC694." }) })
1147
+ ] }),
1148
+ helpOpen ? /* @__PURE__ */ jsx(HelpPanel, { onClose: () => setHelpOpen(false) }) : null
1149
+ ] })
1150
+ ] });
1151
+ }
1152
+ var ViewerBoundary = class extends React.Component {
1153
+ state = { error: null };
1154
+ static getDerivedStateFromError(error) {
1155
+ return { error };
1156
+ }
1157
+ componentDidUpdate(prev) {
1158
+ if (this.state.error && prev.resetKey !== this.props.resetKey) this.setState({ error: null });
1159
+ }
1160
+ render() {
1161
+ if (this.state.error) {
1162
+ return this.props.fallback(this.state.error, () => this.setState({ error: null }));
1163
+ }
1164
+ return this.props.children;
1165
+ }
1166
+ };
1167
+ function ViewerView({ store, id, onBack, onEdit, onNavigate }) {
1168
+ const [state, setState] = useState({
1169
+ spec: null,
1170
+ error: null,
1171
+ seq: 0
1172
+ });
1173
+ const load = useCallback(() => {
1174
+ store.read(id).then(
1175
+ (record) => setState(
1176
+ (prev) => record ? { spec: record.spec, error: null, seq: prev.seq + 1 } : { spec: null, error: "\uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4", seq: prev.seq + 1 }
1177
+ )
1178
+ ).catch(
1179
+ (err) => setState((prev) => ({ spec: null, error: messageOf(err, "\uD398\uC774\uC9C0\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"), seq: prev.seq + 1 }))
1180
+ );
1181
+ }, [store, id]);
1182
+ useEffect(() => {
1183
+ if (!id) {
1184
+ setState((prev) => ({ spec: null, error: "\uD398\uC774\uC9C0 id\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", seq: prev.seq + 1 }));
1185
+ return;
1186
+ }
1187
+ setState((prev) => ({ spec: null, error: null, seq: prev.seq + 1 }));
1188
+ load();
1189
+ }, [id, load]);
1190
+ const adapter = useMemo(
1191
+ () => ({ navigate: onNavigate, refresh: load }),
1192
+ [onNavigate, load]
1193
+ );
1194
+ useEffect(() => {
1195
+ if (state.spec?.title) {
1196
+ const previous = document.title;
1197
+ document.title = state.spec.title;
1198
+ return () => {
1199
+ document.title = previous;
1200
+ };
1201
+ }
1202
+ return void 0;
1203
+ }, [state.spec]);
1204
+ return /* @__PURE__ */ jsxs("div", { className: "sdui-editor-viewer wp-app", children: [
1205
+ /* @__PURE__ */ jsxs("div", { className: "sdui-editor-viewer__bar", children: [
1206
+ /* @__PURE__ */ jsx(IconButton, { icon: "arrow-left", label: "\uBAA9\uB85D\uC73C\uB85C", size: "sm", onClick: onBack }),
1207
+ /* @__PURE__ */ jsxs("span", { className: "wp-mono-xs sdui-editor-viewer__id", children: [
1208
+ "/",
1209
+ id
1210
+ ] }),
1211
+ /* @__PURE__ */ jsx("span", { className: "wp-spacer" }),
1212
+ /* @__PURE__ */ jsx(Button, { size: "sm", icon: "pencil", onClick: () => onEdit(id), children: "\uD3B8\uC9D1" })
1213
+ ] }),
1214
+ /* @__PURE__ */ jsx("div", { className: "sdui-editor-viewer__body", children: state.error ? /* @__PURE__ */ jsx(Alert, { tone: "danger", title: "\uD398\uC774\uC9C0\uB97C \uD45C\uC2DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4", children: state.error }) : !state.spec ? /* @__PURE__ */ jsx("div", { className: "sdui-editor-center", style: { padding: 64 }, children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: "\uBD88\uB7EC\uC624\uB294 \uC911" }) }) : /* @__PURE__ */ jsx(
1215
+ ViewerBoundary,
1216
+ {
1217
+ resetKey: `${id}#${state.seq}`,
1218
+ fallback: (error, retry) => /* @__PURE__ */ jsxs(
1219
+ Alert,
1220
+ {
1221
+ tone: "danger",
1222
+ title: "\uC774 \uD398\uC774\uC9C0 \uC2A4\uD399\uC5D0 \uBB38\uC81C\uAC00 \uC788\uC2B5\uB2C8\uB2E4",
1223
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
1224
+ /* @__PURE__ */ jsx(Button, { size: "xs", icon: "pencil", onClick: () => onEdit(id), children: "\uD3B8\uC9D1\uAE30\uB85C \uC774\uB3D9" }),
1225
+ /* @__PURE__ */ jsx(Button, { size: "xs", icon: "refresh-cw", onClick: retry, children: "\uB2E4\uC2DC \uC2DC\uB3C4" }),
1226
+ /* @__PURE__ */ jsx(Button, { size: "xs", icon: "arrow-left", onClick: onBack, children: "\uBAA9\uB85D\uC73C\uB85C" })
1227
+ ] }),
1228
+ children: [
1229
+ "\uD654\uBA74\uC744 \uADF8\uB9AC\uB294 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC2A4\uD399\uC744 \uACE0\uCE58\uBA74 \uB2E4\uC2DC \uD45C\uC2DC\uB429\uB2C8\uB2E4.",
1230
+ /* @__PURE__ */ jsx("div", { className: "wp-mono-xs", style: { marginTop: 6 }, children: error.message })
1231
+ ]
1232
+ }
1233
+ ),
1234
+ children: /* @__PURE__ */ jsx(SduiRenderer, { page: state.spec, adapter })
1235
+ }
1236
+ ) })
1237
+ ] });
1238
+ }
1239
+ function SduiEditor({
1240
+ store,
1241
+ view,
1242
+ onViewChange,
1243
+ defaultView,
1244
+ chrome = true,
1245
+ onNavigate,
1246
+ className
1247
+ }) {
1248
+ const [internal, setInternal] = useState(defaultView ?? { name: "list" });
1249
+ const current = view ?? internal;
1250
+ const go = useCallback(
1251
+ (next) => {
1252
+ if (view === void 0) setInternal(next);
1253
+ onViewChange?.(next);
1254
+ },
1255
+ [view, onViewChange]
1256
+ );
1257
+ const openList = useCallback(() => go({ name: "list" }), [go]);
1258
+ const openEdit = useCallback((id) => go({ name: "edit", id }), [go]);
1259
+ const openPreview = useCallback((id) => go({ name: "preview", id }), [go]);
1260
+ const navigateFromSpec = useCallback(
1261
+ (to) => {
1262
+ if (onNavigate) {
1263
+ onNavigate(to);
1264
+ return;
1265
+ }
1266
+ if (to.startsWith("/")) openPreview(to.replace(/^\/+/, ""));
1267
+ },
1268
+ [onNavigate, openPreview]
1269
+ );
1270
+ const caps = store.capabilities;
1271
+ return /* @__PURE__ */ jsxs("div", { className: `sdui-editor${className ? ` ${className}` : ""}`, children: [
1272
+ chrome ? /* @__PURE__ */ jsxs("header", { className: "sdui-editor-chrome", children: [
1273
+ /* @__PURE__ */ jsx("span", { className: "sdui-editor-chrome__title", children: "SDUI \uD398\uC774\uC9C0 \uD3B8\uC9D1\uAE30" }),
1274
+ /* @__PURE__ */ jsx(Badge, { tone: "brand", title: caps.label, children: caps.label })
1275
+ ] }) : null,
1276
+ current.name === "list" ? /* @__PURE__ */ jsx(PageListView, { store, onEdit: openEdit, onPreview: openPreview }) : current.name === "edit" ? /* @__PURE__ */ jsx(EditorView2, { store, id: current.id, onBack: openList, onPreview: openPreview }) : /* @__PURE__ */ jsx(
1277
+ ViewerView,
1278
+ {
1279
+ store,
1280
+ id: current.id,
1281
+ onBack: openList,
1282
+ onEdit: openEdit,
1283
+ onNavigate: navigateFromSpec
1284
+ }
1285
+ )
1286
+ ] });
1287
+ }
1288
+
1289
+ export { EditorView2 as EditorView, PAGE_ID_PATTERN, PageExistsError, PageGoneError, PageIdConflictError, PageIdRejectedError, PageListView, RestSpecStore, SduiEditor, StoreUnauthorizedError, StoreUnavailableError, StoreUnsupportedError, TEMPLATES, VersionMismatchError, ViewerView, createRestSpecStore, gradeIssues, isValidPageId, titleOf };