@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/README.md +27 -0
- package/dist/index.d.ts +364 -0
- package/dist/index.js +1289 -0
- package/dist/styles.css +147 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @wipco/sdui-editor
|
|
2
|
+
|
|
3
|
+
SDUI 페이지 스펙 편집기 — 목록 · JSON 편집(실시간 미리보기) · 뷰어.
|
|
4
|
+
|
|
5
|
+
**앱이 아니라 컴포넌트다.** 소비 프로젝트가 자기 라우트에 마운트하면 같은 오리진이라
|
|
6
|
+
CORS·쿠키 문제가 없고, 그 프로젝트의 세션·권한을 그대로 쓴다. 그래서 이 패키지에는
|
|
7
|
+
로그인 화면도 라우터도 없다.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { SduiEditor, createRestSpecStore } from '@wipco/sdui-editor';
|
|
11
|
+
import '@wipco/sdui/styles.css';
|
|
12
|
+
import '@wipco/sdui-editor/styles.css';
|
|
13
|
+
|
|
14
|
+
// 같은 오리진의 /ui/* 를 스펙 저장소로 쓴다. 렌더마다 새로 만들지 말 것.
|
|
15
|
+
const store = createRestSpecStore();
|
|
16
|
+
|
|
17
|
+
export function SpecEditorRoute() {
|
|
18
|
+
return <SduiEditor store={store} />;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- 화면 전환은 기본이 내부 상태다. 주소에 실으려면 `view`/`onViewChange`로 가져가면 된다.
|
|
23
|
+
- 저장소는 `SpecStore` 인터페이스(패키지에서 타입으로 export)만 만족하면 무엇이든 된다.
|
|
24
|
+
- 저장소가 `capabilities`로 못 하는 일을 밝히면 편집기가 화면을 그에 맞게 재단한다.
|
|
25
|
+
|
|
26
|
+
자세한 것은 [저장소 README](https://github.com/wipco-dev/wipco-sdui#readme) 참고.
|
|
27
|
+
(tarball에는 dist만 담기므로 상대 경로 링크는 npm 페이지에서 깨진다.)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import react__default from 'react';
|
|
3
|
+
import { SpecIssue } from '@wipco/sdui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 스펙 저장소 어댑터 계약.
|
|
7
|
+
*
|
|
8
|
+
* 편집기는 스펙이 **어디에** 사는지 몰라야 한다. ERP의 DB든 다른 소비 프로젝트의
|
|
9
|
+
* API든 같은 인터페이스 뒤에 있으면 편집 화면을 그대로 두고 저장소만 갈아끼울 수 있다.
|
|
10
|
+
*
|
|
11
|
+
* 예전에는 이 계약이 Express 서버 안(`server/src/store/`)에 있었고 편집기는 그 서버의
|
|
12
|
+
* `/api/pages`만 알았다. 서버를 없애면서 계약을 편집기 패키지로 옮겼다 —
|
|
13
|
+
* **저장소는 이제 소비 프로젝트가 `<SduiEditor store={...} />`로 주입한다.**
|
|
14
|
+
* 브라우저에서 직접 호출하므로 편집기는 소비 프로젝트의 세션(쿠키)을 그대로 쓰고,
|
|
15
|
+
* 같은 오리진이라 CORS도 없다.
|
|
16
|
+
*
|
|
17
|
+
* 기본 구현은 하나다:
|
|
18
|
+
* - {@link ./rest-store.js REST 어댑터} — 소비 프로젝트(ERP)의 `/ui/*`를 호출한다.
|
|
19
|
+
* 정본은 소비 프로젝트에 있고 편집기는 사본을 갖지 않는다.
|
|
20
|
+
*
|
|
21
|
+
* (파일 어댑터는 삭제했다. Node의 fs에 기대던 구현이라 브라우저에 갈 곳이 없고,
|
|
22
|
+
* 그 저장소를 서빙하던 Express 서버도 함께 사라졌다.)
|
|
23
|
+
*
|
|
24
|
+
* ## 버전 토큰(낙관적 잠금)을 인터페이스가 다루는 이유
|
|
25
|
+
*
|
|
26
|
+
* "읽어 간 뒤 남이 먼저 저장했는가"는 저장소마다 **막을 수 있는 지점이 다르다**.
|
|
27
|
+
* 원격 서버가 조건부 쓰기를 지원해야만 원자적으로 막을 수 있고, 아니면 읽기→비교→쓰기로
|
|
28
|
+
* 흉내 낼 수밖에 없다. 이 차이를 호출부에 흘리면 편집 화면이 저장소 종류를 알게 되므로,
|
|
29
|
+
* 기대값을 {@link WriteOptions.expect}로 받아 **각 구현이 자기 방식으로** 검사하고
|
|
30
|
+
* 실패를 공통 예외로 던지게 했다.
|
|
31
|
+
*
|
|
32
|
+
* 토큰 값 자체는 저장소가 정하고 호출부는 해석하지 않는다.
|
|
33
|
+
*/
|
|
34
|
+
interface PageMeta {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* 사람이 읽는 표시용 시각. 동시성 비교에는 쓰지 않는다 — {@link version}을 쓴다.
|
|
39
|
+
* 알 수 없으면 빈 문자열이다(ERP의 단건 조회는 시각을 주지 않는다).
|
|
40
|
+
*/
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
/** 낙관적 동시성 토큰. 저장소가 정한 불투명 값이라 호출부는 해석하지 않는다. */
|
|
43
|
+
version: string;
|
|
44
|
+
/** 스펙을 읽지 못한 항목(파싱 실패 등). 목록에서 지우면 열 수도 지울 수도 없는 유령 행이 된다. */
|
|
45
|
+
corrupt?: boolean;
|
|
46
|
+
}
|
|
47
|
+
interface PageRecord extends PageMeta {
|
|
48
|
+
/** corrupt인 항목은 파싱 결과가 없으므로 null이다. */
|
|
49
|
+
spec: unknown;
|
|
50
|
+
}
|
|
51
|
+
/** 이력 한 건 — 저장소가 이력을 지원할 때만 나온다. */
|
|
52
|
+
interface PageRevision {
|
|
53
|
+
/** 저장소 안에서 단조 증가하는 리비전 식별자 */
|
|
54
|
+
revision: string;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
/** 저장한 사람(알 수 없으면 null) */
|
|
57
|
+
updatedBy: string | null;
|
|
58
|
+
title: string | null;
|
|
59
|
+
}
|
|
60
|
+
/** 저장 시 기대하는 현재 상태 — 어긋나면 {@link VersionMismatchError}. */
|
|
61
|
+
type VersionExpectation = {
|
|
62
|
+
kind: 'version';
|
|
63
|
+
value: string;
|
|
64
|
+
};
|
|
65
|
+
interface WriteOptions {
|
|
66
|
+
/** 생성 전용 — 이미 있으면 {@link PageExistsError}. */
|
|
67
|
+
ifAbsent?: boolean;
|
|
68
|
+
/** 낙관적 잠금 기준. 없으면 무조건 덮어쓴다(업서트). */
|
|
69
|
+
expect?: VersionExpectation;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 저장소가 실제로 할 수 있는 일. 편집기가 이 값을 보고 화면을 재단한다 —
|
|
73
|
+
* 누를 수 없는 버튼을 띄워 놓고 오류를 되돌려주는 것보다, 애초에 감추는 편이 낫다.
|
|
74
|
+
*
|
|
75
|
+
* 예전에는 서버가 `GET /api/store`로 내려 주던 값이라 편집기가 로딩 상태(null)를
|
|
76
|
+
* 따로 다뤄야 했다. 이제 저장소 객체를 직접 들고 있으므로 **동기 속성**이다 —
|
|
77
|
+
* "능력을 아직 모르는 순간"이 아예 없어졌다.
|
|
78
|
+
*/
|
|
79
|
+
interface StoreCapabilities {
|
|
80
|
+
kind: 'rest' | (string & {});
|
|
81
|
+
/** 사람이 읽는 저장소 이름 — 편집기 배너에 그대로 나간다 */
|
|
82
|
+
label: string;
|
|
83
|
+
/** 삭제 지원 여부. ERP에는 `DELETE /ui/pages/{id}`가 없어 false다. */
|
|
84
|
+
remove: boolean;
|
|
85
|
+
/** 생성 전용 저장을 **경합 없이** 보장하는가. false면 존재 검사와 쓰기 사이에 틈이 있다. */
|
|
86
|
+
atomicCreate: boolean;
|
|
87
|
+
/** 낙관적 잠금을 **원자적으로** 보장하는가. false면 검사와 쓰기 사이에 틈이 있다. */
|
|
88
|
+
atomicLocking: boolean;
|
|
89
|
+
/** 이력 조회 지원 여부 */
|
|
90
|
+
history: boolean;
|
|
91
|
+
/** 편집기가 사용자에게 보여야 할 제약 문구(빈 배열이면 제약 없음) */
|
|
92
|
+
limitations: string[];
|
|
93
|
+
}
|
|
94
|
+
interface SpecStore {
|
|
95
|
+
readonly capabilities: StoreCapabilities;
|
|
96
|
+
list(): Promise<PageMeta[]>;
|
|
97
|
+
/** 없으면 null. 스펙을 읽지 못한 항목은 null이 아니라 `corrupt: true` 레코드다. */
|
|
98
|
+
read(id: string): Promise<PageRecord | null>;
|
|
99
|
+
write(id: string, spec: unknown, options?: WriteOptions): Promise<PageRecord>;
|
|
100
|
+
/** 지웠으면 true, 원래 없었으면 false. 지원하지 않으면 {@link StoreUnsupportedError}. */
|
|
101
|
+
remove(id: string): Promise<boolean>;
|
|
102
|
+
/** 이력 조회. `capabilities.history`가 false면 구현이 없다. */
|
|
103
|
+
history?(id: string): Promise<PageRevision[]>;
|
|
104
|
+
}
|
|
105
|
+
/** 대소문자만 다른 id가 이미 있을 때 — 같은 대상을 가리켜 기존 페이지를 파괴한다 */
|
|
106
|
+
declare class PageIdConflictError extends Error {
|
|
107
|
+
readonly existingId: string;
|
|
108
|
+
constructor(existingId: string);
|
|
109
|
+
}
|
|
110
|
+
/** 생성 전용 요청인데 이미 있을 때 */
|
|
111
|
+
declare class PageExistsError extends Error {
|
|
112
|
+
readonly id: string;
|
|
113
|
+
constructor(id: string);
|
|
114
|
+
}
|
|
115
|
+
/** 기준 버전을 보낸 저장인데 그 사이 다른 저장이 있었을 때 */
|
|
116
|
+
declare class VersionMismatchError extends Error {
|
|
117
|
+
readonly current: PageMeta;
|
|
118
|
+
constructor(current: PageMeta);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 기준 버전을 보낸 저장인데 대상이 이미 삭제됐을 때.
|
|
122
|
+
* 조용히 업서트하면 다른 관리자가 지운 페이지가 되살아난다.
|
|
123
|
+
*/
|
|
124
|
+
declare class PageGoneError extends Error {
|
|
125
|
+
readonly id: string;
|
|
126
|
+
constructor(id: string);
|
|
127
|
+
}
|
|
128
|
+
/** 이 저장소가 지원하지 않는 동작(예: ERP에는 삭제 경로가 없다) */
|
|
129
|
+
declare class StoreUnsupportedError extends Error {
|
|
130
|
+
readonly operation: string;
|
|
131
|
+
constructor(operation: string, detail: string);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 저장소 자체에 닿지 못했을 때(원격 서버 다운·권한 부족 등).
|
|
135
|
+
* "내 화면이 고장났다"와 "상류가 문제다"는 사용자가 할 일이 다르므로 따로 둔다.
|
|
136
|
+
*/
|
|
137
|
+
declare class StoreUnavailableError extends Error {
|
|
138
|
+
readonly cause?: unknown | undefined;
|
|
139
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 소비 프로젝트의 세션이 없거나 만료됐을 때.
|
|
143
|
+
*
|
|
144
|
+
* 편집기는 로그인 화면을 갖지 않는다(소비 프로젝트의 세션을 쓴다). 그래서 401을
|
|
145
|
+
* 저장소 장애로 뭉뚱그리면 사용자가 "다시 로그인하라"는 안내를 못 받는다 —
|
|
146
|
+
* 별도 예외로 두어 편집기가 그 문장을 띄우고, 소비 프로젝트는
|
|
147
|
+
* {@link RestStoreConfig.onUnauthorized}로 자기 로그인 흐름을 태울 수 있다.
|
|
148
|
+
*/
|
|
149
|
+
declare class StoreUnauthorizedError extends Error {
|
|
150
|
+
constructor(message: string);
|
|
151
|
+
}
|
|
152
|
+
/** 저장소가 id 자체를 받지 않을 때(예: ERP의 `_` 접두 id) */
|
|
153
|
+
declare class PageIdRejectedError extends Error {
|
|
154
|
+
readonly id: string;
|
|
155
|
+
constructor(id: string, detail: string);
|
|
156
|
+
}
|
|
157
|
+
/** 페이지 id 규칙: [A-Za-z0-9_-] 세그먼트를 /로 연결. 경로 탈출을 원천 차단한다. */
|
|
158
|
+
declare const PAGE_ID_PATTERN: RegExp;
|
|
159
|
+
declare function isValidPageId(id: string): boolean;
|
|
160
|
+
/** 스펙에서 표시용 제목 뽑기 — 모든 어댑터가 같은 규칙을 써야 목록이 갈리지 않는다 */
|
|
161
|
+
declare function titleOf(spec: unknown): string | null;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* REST 어댑터 — 소비 프로젝트(ERP)의 `/ui/*`를 스펙 저장소로 쓴다.
|
|
165
|
+
*
|
|
166
|
+
* **정본은 소비 프로젝트에 있고 편집기는 사본을 갖지 않는다.** 저장하면 그 즉시
|
|
167
|
+
* `ui_specs`가 바뀌고, 이력(`ui_spec_revisions`) 적재와 SSE(`ui_spec`) 발행은
|
|
168
|
+
* 소비 프로젝트가 알아서 한다 — 여기서 흉내 내지 않는다.
|
|
169
|
+
*
|
|
170
|
+
* ## 브라우저에서 돈다 — 인증을 갖지 않는 이유
|
|
171
|
+
*
|
|
172
|
+
* 예전 구현은 Express 서버 안에서 돌았고 서비스 계정으로 ERP에 로그인해 Bearer 토큰을
|
|
173
|
+
* 관리했다(만료 시 재로그인·동시 로그인 억제까지). 편집기를 소비 프로젝트에 마운트하는
|
|
174
|
+
* 지금은 그 전부가 **필요 없을 뿐 아니라 해롭다**:
|
|
175
|
+
*
|
|
176
|
+
* - 같은 오리진이므로 소비 프로젝트의 세션 쿠키가 그대로 실린다(`credentials`).
|
|
177
|
+
* - 서비스 계정을 쓰면 ERP의 `updated_by`가 전원 같은 계정으로 남아 **누가 고쳤는지**
|
|
178
|
+
* 가 사라진다. 지금은 로그인한 그 사람으로 남는다.
|
|
179
|
+
* - 브라우저에 서비스 계정 비밀번호를 둘 방법은 없다.
|
|
180
|
+
*
|
|
181
|
+
* 그래서 이 어댑터는 자격증명을 **모른다**. 필요하면 소비 프로젝트가
|
|
182
|
+
* {@link RestStoreConfig.headers}로 실어 주고(Bearer 등), 401은 그대로
|
|
183
|
+
* {@link StoreUnauthorizedError}로 올려 소비 프로젝트의 로그인 흐름에 넘긴다.
|
|
184
|
+
*
|
|
185
|
+
* ## ERP 실계약
|
|
186
|
+
* | 동작 | ERP 경로 | 비고 |
|
|
187
|
+
* | --- | --- | --- |
|
|
188
|
+
* | 목록 | `GET /ui/pages-index` | 제목·수정자·수정시각·etag. 권한 `setting.update` 필요 |
|
|
189
|
+
* | 목록(대체) | `GET /ui/manifest` | `{count, specs:{id:etag}}` — **제목이 없다** |
|
|
190
|
+
* | 읽기 | `GET /ui/pages/{id}` | 스펙 JSON + `ETag`, `If-None-Match` → 304 |
|
|
191
|
+
* | 쓰기 | `PUT /ui/pages/{id}` | `{spec, title?}`, 권한 `setting.update` |
|
|
192
|
+
* | 삭제 | — | **없다.** `DELETE`는 405다 |
|
|
193
|
+
*
|
|
194
|
+
* ## 목록의 제목 간극
|
|
195
|
+
* `/ui/manifest`에는 제목이 없지만 ERP에는 편집기 전용 목록 `GET /ui/pages-index`가
|
|
196
|
+
* 이미 있다(id·title·etag·updatedAt·updatedBy). 매니페스트는 모든 클라이언트가 진입할 때
|
|
197
|
+
* 타는 캐시 핵심 경로라 ERP가 의도적으로 분리해 둔 것이다. 그래서 이 어댑터는
|
|
198
|
+
* pages-index를 쓰고 ERP에 아무 변경도 요구하지 않는다. 그 경로가 없는 구버전에 붙을
|
|
199
|
+
* 수도 있으므로 404면 매니페스트로 물러난다 — 그때는 제목 칸이 비고(`title: null`),
|
|
200
|
+
* 수십 개 스펙을 전부 받아 오는 짓은 하지 않는다(목록 한 번에 수십 회 왕복은
|
|
201
|
+
* 편집기를 못 쓰게 만든다).
|
|
202
|
+
*
|
|
203
|
+
* ## 못 하는 일 (편집기가 화면에서 감춘다 — {@link StoreCapabilities})
|
|
204
|
+
* - **삭제**: ERP에 경로가 없다. `ui_specs.deleted_at` 컬럼은 있으나 노출되지 않는다.
|
|
205
|
+
* - **이력 조회**: `ui_spec_revisions`에 쌓이지만 읽는 HTTP 경로가 없다.
|
|
206
|
+
* - **`_` 접두 id(`_nav`)**: `GET/PUT /ui/pages/_nav`는 404다(내비는 `/ui/nav-spec`이
|
|
207
|
+
* 따로 있다). 여기서는 왕복 전에 막고 이유를 밝힌다 — 그냥 흘려보내면 사용자는
|
|
208
|
+
* "페이지 없음"만 보고 왜인지 알 수 없다.
|
|
209
|
+
* - **원자적 낙관적 잠금**: ERP의 PUT은 `If-Match`를 읽지 않는다(엉뚱한 ETag를 보내도
|
|
210
|
+
* 200). 그래서 여기서 읽기→비교→쓰기로 흉내 내지만 그 사이에 틈이 있다.
|
|
211
|
+
* `capabilities.atomicLocking = false`로 알린다.
|
|
212
|
+
*/
|
|
213
|
+
interface RestStoreConfig {
|
|
214
|
+
/**
|
|
215
|
+
* 소비 프로젝트 백엔드의 베이스 URL. 기본값은 빈 문자열 = **같은 오리진**이다 —
|
|
216
|
+
* 편집기를 소비 프로젝트에 마운트하는 것이 정상 구성이라 그게 기본이어야 한다.
|
|
217
|
+
*/
|
|
218
|
+
baseUrl?: string;
|
|
219
|
+
/** 편집기 배지에 뜨는 이름. 기본값은 baseUrl로 만든다. */
|
|
220
|
+
label?: string;
|
|
221
|
+
/**
|
|
222
|
+
* 쿠키 전송 방식. 같은 오리진이면 기본값 `same-origin`으로 충분하다.
|
|
223
|
+
* 다른 오리진에 붙일 때만 `include`(그리고 상대 쪽 CORS 설정)가 필요하다.
|
|
224
|
+
*/
|
|
225
|
+
credentials?: RequestCredentials;
|
|
226
|
+
/** 요청마다 덧붙일 헤더 — 쿠키 대신 Bearer를 쓰는 소비 프로젝트용 */
|
|
227
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
228
|
+
/** 요청 타임아웃(ms) */
|
|
229
|
+
timeoutMs?: number;
|
|
230
|
+
/** 401을 받았을 때 — 소비 프로젝트가 자기 로그인 흐름을 태울 자리 */
|
|
231
|
+
onUnauthorized?: () => void;
|
|
232
|
+
}
|
|
233
|
+
declare class RestSpecStore implements SpecStore {
|
|
234
|
+
readonly capabilities: StoreCapabilities;
|
|
235
|
+
private readonly baseUrl;
|
|
236
|
+
private readonly timeoutMs;
|
|
237
|
+
private readonly config;
|
|
238
|
+
constructor(config?: RestStoreConfig);
|
|
239
|
+
private request;
|
|
240
|
+
/** 상류 오류를 사람이 읽을 수 있는 문장으로 — 상류가 준 메시지가 있으면 그대로 싣는다 */
|
|
241
|
+
private upstreamError;
|
|
242
|
+
list(): Promise<PageMeta[]>;
|
|
243
|
+
read(id: string): Promise<PageRecord | null>;
|
|
244
|
+
write(id: string, spec: unknown, options?: WriteOptions): Promise<PageRecord>;
|
|
245
|
+
remove(id: string): Promise<boolean>;
|
|
246
|
+
}
|
|
247
|
+
/** 같은 오리진의 ERP `/ui/*`를 스펙 저장소로 쓰는 어댑터를 만든다. */
|
|
248
|
+
declare function createRestSpecStore(config?: RestStoreConfig): SpecStore;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 편집기 내부 화면.
|
|
252
|
+
*
|
|
253
|
+
* 자체 라우터를 갖지 않는 이유: 이 컴포넌트는 **소비 프로젝트의 라우트 안에 마운트**된다.
|
|
254
|
+
* BrowserRouter를 하나 더 세우면 소비 프로젝트의 라우터와 히스토리를 두고 다투고,
|
|
255
|
+
* 주소창은 편집기가 어디에 있는지 모르는 채로 남는다. 그래서 화면 전환은
|
|
256
|
+
* 기본적으로 내부 상태로 하고, 주소에 실어야 하는 소비 프로젝트는
|
|
257
|
+
* {@link SduiEditorProps.view}/{@link SduiEditorProps.onViewChange}로 가져가면 된다.
|
|
258
|
+
*/
|
|
259
|
+
type SduiEditorView = {
|
|
260
|
+
name: 'list';
|
|
261
|
+
} | {
|
|
262
|
+
name: 'edit';
|
|
263
|
+
id: string;
|
|
264
|
+
} | {
|
|
265
|
+
name: 'preview';
|
|
266
|
+
id: string;
|
|
267
|
+
};
|
|
268
|
+
interface SduiEditorProps {
|
|
269
|
+
/** 스펙 저장소 — 소비 프로젝트가 주입한다. {@link createRestSpecStore} 참고. */
|
|
270
|
+
store: SpecStore;
|
|
271
|
+
/** 제어 모드: 소비 프로젝트가 화면 전환을 소유할 때 (자기 라우터·주소에 싣는 경우) */
|
|
272
|
+
view?: SduiEditorView;
|
|
273
|
+
/** 제어·비제어 양쪽에서 화면이 바뀔 때마다 불린다 */
|
|
274
|
+
onViewChange?: (view: SduiEditorView) => void;
|
|
275
|
+
/** 비제어 모드의 초기 화면 */
|
|
276
|
+
defaultView?: SduiEditorView;
|
|
277
|
+
/** 편집기 자체 헤더(저장소 배지)를 그릴지. 소비 프로젝트 크롬이 이미 있으면 false */
|
|
278
|
+
chrome?: boolean;
|
|
279
|
+
/**
|
|
280
|
+
* 뷰어에서 스펙의 `navigate` 액션이 나왔을 때. 주지 않으면 뷰어 안에서 그 페이지를
|
|
281
|
+
* 열어 본다(절대 URL은 무시한다 — 편집기를 마운트한 화면을 날려 버리지 않기 위해).
|
|
282
|
+
*/
|
|
283
|
+
onNavigate?: (to: string) => void;
|
|
284
|
+
className?: string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* SDUI 페이지 스펙 편집기 — 목록·JSON 편집(실시간 미리보기)·뷰어.
|
|
288
|
+
*
|
|
289
|
+
* 앱이 아니라 **마운트하는 컴포넌트**다. 소비 프로젝트가 자기 라우트에 달면 같은
|
|
290
|
+
* 오리진이라 CORS·쿠키 문제가 없고, 그 프로젝트의 세션·권한을 그대로 쓴다.
|
|
291
|
+
* 그래서 이 컴포넌트에는 로그인 화면이 없다 — 인증은 소비 프로젝트가 이미 끝낸 일이다.
|
|
292
|
+
*
|
|
293
|
+
* ```tsx
|
|
294
|
+
* import { SduiEditor, createRestSpecStore } from '@wipco/sdui-editor';
|
|
295
|
+
* import '@wipco/sdui/styles.css';
|
|
296
|
+
* import '@wipco/sdui-editor/styles.css';
|
|
297
|
+
*
|
|
298
|
+
* const store = createRestSpecStore(); // 같은 오리진의 /ui/*
|
|
299
|
+
* <SduiEditor store={store} />
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
declare function SduiEditor({ store, view, onViewChange, defaultView, chrome, onNavigate, className, }: SduiEditorProps): react.JSX.Element;
|
|
303
|
+
|
|
304
|
+
interface PageListViewProps {
|
|
305
|
+
store: SpecStore;
|
|
306
|
+
onEdit: (id: string) => void;
|
|
307
|
+
onPreview: (id: string) => void;
|
|
308
|
+
}
|
|
309
|
+
declare function PageListView({ store, onEdit, onPreview }: PageListViewProps): react.JSX.Element;
|
|
310
|
+
|
|
311
|
+
interface EditorViewProps {
|
|
312
|
+
store: SpecStore;
|
|
313
|
+
id: string;
|
|
314
|
+
onBack: () => void;
|
|
315
|
+
onPreview: (id: string) => void;
|
|
316
|
+
}
|
|
317
|
+
declare function EditorView({ store, id, onBack, onPreview }: EditorViewProps): react__default.JSX.Element;
|
|
318
|
+
|
|
319
|
+
interface ViewerViewProps {
|
|
320
|
+
store: SpecStore;
|
|
321
|
+
id: string;
|
|
322
|
+
onBack: () => void;
|
|
323
|
+
onEdit: (id: string) => void;
|
|
324
|
+
/**
|
|
325
|
+
* 스펙 안의 `navigate` 액션 처리.
|
|
326
|
+
*
|
|
327
|
+
* 화면 전환은 상위(편집기 셸 또는 소비 프로젝트)가 소유하므로 뷰어가 스스로
|
|
328
|
+
* 결정하지 않는다 — 여기서 window.location을 만지면 편집기를 마운트한 페이지가
|
|
329
|
+
* 통째로 날아가고 저장 안 한 초안이 함께 사라진다.
|
|
330
|
+
*/
|
|
331
|
+
onNavigate: (to: string) => void;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* 페이지 뷰어 — 저장된 스펙을 실제 소비 화면처럼 렌더링한다.
|
|
335
|
+
*
|
|
336
|
+
* 편집기 미리보기와 달리 **어댑터를 막지 않는다**. 여기서 보는 것은 편집 중인 텍스트가
|
|
337
|
+
* 아니라 이미 저장된 스펙이고, 소비 프로젝트가 실제로 어떻게 그릴지 확인하는 자리다.
|
|
338
|
+
* 다만 부작용(submitForm 등)은 진짜로 나가므로 소비 프로젝트가 어댑터를 주입해
|
|
339
|
+
* 원하는 만큼만 열어 주면 된다.
|
|
340
|
+
*/
|
|
341
|
+
declare function ViewerView({ store, id, onBack, onEdit, onNavigate }: ViewerViewProps): react__default.JSX.Element;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 새 페이지 생성 템플릿 — 스펙 문법 견본을 겸한다.
|
|
345
|
+
*
|
|
346
|
+
* 폼 템플릿의 `endpoint`는 `/api/example`이라는 **자리 표시자**다. 예전에는 편집 서버가
|
|
347
|
+
* 갖고 있던 `/api/echo`를 가리켰지만 그 서버가 사라졌고, 무엇보다 편집기는 이제
|
|
348
|
+
* 소비 프로젝트 안에서 돈다 — 여기에 실재하는 경로를 적어 두면 새로 만든 페이지가
|
|
349
|
+
* 저자도 모르는 사이에 **운영 API를 때린다**. 저자가 반드시 자기 경로로 바꾸게 둔다.
|
|
350
|
+
*/
|
|
351
|
+
interface PageTemplate {
|
|
352
|
+
key: string;
|
|
353
|
+
label: string;
|
|
354
|
+
description: string;
|
|
355
|
+
spec: unknown;
|
|
356
|
+
}
|
|
357
|
+
declare const TEMPLATES: PageTemplate[];
|
|
358
|
+
|
|
359
|
+
type GradedIssue = SpecIssue & {
|
|
360
|
+
severity: 'error' | 'warning';
|
|
361
|
+
};
|
|
362
|
+
declare function gradeIssues(spec: unknown): GradedIssue[];
|
|
363
|
+
|
|
364
|
+
export { EditorView, type EditorViewProps, type GradedIssue, PAGE_ID_PATTERN, PageExistsError, PageGoneError, PageIdConflictError, PageIdRejectedError, PageListView, type PageListViewProps, type PageMeta, type PageRecord, type PageRevision, type PageTemplate, RestSpecStore, type RestStoreConfig, SduiEditor, type SduiEditorProps, type SduiEditorView, type SpecStore, type StoreCapabilities, StoreUnauthorizedError, StoreUnavailableError, StoreUnsupportedError, TEMPLATES, type VersionExpectation, VersionMismatchError, ViewerView, type ViewerViewProps, type WriteOptions, createRestSpecStore, gradeIssues, isValidPageId, titleOf };
|