@ssobig/writer-cli 0.2.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 +35 -0
- package/asset-repository.js +278 -0
- package/config.js +14 -0
- package/package.json +28 -0
- package/project-runtime.js +102 -0
- package/storage-path.js +110 -0
- package/templates/mystery-v1/authoring-view-preference.js +34 -0
- package/templates/mystery-v1/character-perspective-preview.js +61 -0
- package/templates/mystery-v1/component-asset-operations.js +103 -0
- package/templates/mystery-v1/component-autosave.js +121 -0
- package/templates/mystery-v1/component-catalog-contract.js +340 -0
- package/templates/mystery-v1/component-checkpoint-history.js +145 -0
- package/templates/mystery-v1/component-contract.js +90 -0
- package/templates/mystery-v1/component-draft-operations.js +313 -0
- package/templates/mystery-v1/component-field-contracts.js +595 -0
- package/templates/mystery-v1/component-id-policy.js +64 -0
- package/templates/mystery-v1/component-manager.js +396 -0
- package/templates/mystery-v1/component-navigation-counts.js +64 -0
- package/templates/mystery-v1/component-registry.js +205 -0
- package/templates/mystery-v1/component-renderers.js +139 -0
- package/templates/mystery-v1/component-storage-contract.js +237 -0
- package/templates/mystery-v1/external-update-coordinator.js +91 -0
- package/templates/mystery-v1/output-clue-card-layout.js +46 -0
- package/templates/mystery-v1/page-header.js +26 -0
- package/templates/mystery-v1/render-ui-state.js +76 -0
- package/templates/mystery-v1/runtime-snapshot-reconciler.js +40 -0
- package/templates/mystery-v1/tab-bar.js +87 -0
- package/templates/mystery-v1/view-component-contract.js +152 -0
- package/templates/mystery-v1/view-component-registry.js +44 -0
- package/templates/mystery-v1/view-component-runtime.js +95 -0
- package/tools/writer-cli/bin/ssobig-writer-daemon.cjs +34 -0
- package/tools/writer-cli/bin/ssobig-writer.cjs +12 -0
- package/tools/writer-cli/package-lock.json +121 -0
- package/tools/writer-cli/package.json +22 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +38 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/agents/openai.yaml +4 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +10 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +7 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +5 -0
- package/tools/writer-cli/src/agent-paths.cjs +114 -0
- package/tools/writer-cli/src/agent-service.cjs +496 -0
- package/tools/writer-cli/src/asset-policy.cjs +113 -0
- package/tools/writer-cli/src/auth.cjs +655 -0
- package/tools/writer-cli/src/checkpoint-diff.cjs +128 -0
- package/tools/writer-cli/src/command-registry.cjs +152 -0
- package/tools/writer-cli/src/commands.cjs +841 -0
- package/tools/writer-cli/src/corpus.cjs +83 -0
- package/tools/writer-cli/src/daemon-app.cjs +106 -0
- package/tools/writer-cli/src/daemon-client.cjs +187 -0
- package/tools/writer-cli/src/daemon-protocol.cjs +184 -0
- package/tools/writer-cli/src/daemon-runner.cjs +97 -0
- package/tools/writer-cli/src/daemon-server.cjs +378 -0
- package/tools/writer-cli/src/diagnostics.cjs +235 -0
- package/tools/writer-cli/src/domain.cjs +731 -0
- package/tools/writer-cli/src/errors.cjs +47 -0
- package/tools/writer-cli/src/gateway.cjs +357 -0
- package/tools/writer-cli/src/investigation-board-layout.cjs +328 -0
- package/tools/writer-cli/src/json-patch.cjs +98 -0
- package/tools/writer-cli/src/json.cjs +26 -0
- package/tools/writer-cli/src/local-index-cache.cjs +139 -0
- package/tools/writer-cli/src/local-index-lookup.cjs +98 -0
- package/tools/writer-cli/src/local-index-query.cjs +304 -0
- package/tools/writer-cli/src/local-index-snapshot.cjs +235 -0
- package/tools/writer-cli/src/local-index-storage.cjs +284 -0
- package/tools/writer-cli/src/local-index.cjs +199 -0
- package/tools/writer-cli/src/mutations.cjs +722 -0
- package/tools/writer-cli/src/platform-runner.cjs +55 -0
- package/tools/writer-cli/src/project-import.cjs +485 -0
- package/tools/writer-cli/src/skill-manager.cjs +255 -0
- package/tools/writer-cli/src/source-fingerprint.cjs +90 -0
- package/tools/writer-cli/src/update-gate.cjs +102 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
let contract = root && root.WriterWorkbenchComponentContract;
|
|
3
|
+
let fieldContracts = root && root.WriterWorkbenchComponentFieldContracts;
|
|
4
|
+
let catalogContract = root && root.WriterWorkbenchComponentCatalogContract;
|
|
5
|
+
if (typeof module === "object" && module.exports) {
|
|
6
|
+
contract = require("./component-contract.js");
|
|
7
|
+
fieldContracts = require("./component-field-contracts.js");
|
|
8
|
+
catalogContract = require("./component-catalog-contract.js");
|
|
9
|
+
}
|
|
10
|
+
const api = factory(contract, fieldContracts, catalogContract);
|
|
11
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
12
|
+
if (root) root.WriterWorkbenchComponentRegistry = api;
|
|
13
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (contract, fieldContracts, catalogContract) {
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
if (!contract) throw new Error("WriterWorkbenchComponentContract is required");
|
|
17
|
+
if (!fieldContracts) throw new Error("WriterWorkbenchComponentFieldContracts is required");
|
|
18
|
+
if (!catalogContract) throw new Error("WriterWorkbenchComponentCatalogContract is required");
|
|
19
|
+
|
|
20
|
+
const { WORKSPACE_MODES, normalizeVariantId, assertVariantId } = contract;
|
|
21
|
+
|
|
22
|
+
function deepFreeze(value) {
|
|
23
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
24
|
+
Object.values(value).forEach(deepFreeze);
|
|
25
|
+
return Object.freeze(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clonePlain(value) {
|
|
29
|
+
if (Array.isArray(value)) return value.map(clonePlain);
|
|
30
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clonePlain(item)]));
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function view(id, label, kind = id, options = {}) {
|
|
35
|
+
return Object.freeze({ id, label, kind, implemented: options.implemented !== false, description: String(options.description || "") });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const genericDesignViews = Object.freeze([view("theme", "테마", "design")]);
|
|
39
|
+
const genericValidationViews = Object.freeze([view("schema", "구조 검사", "validation")]);
|
|
40
|
+
const directAuthoring = Object.freeze([view("json", "직접 JSON", "json")]);
|
|
41
|
+
const rawPreview = Object.freeze([view("raw", "원본 데이터", "json")]);
|
|
42
|
+
|
|
43
|
+
const CLUE_FIELD_CONFIGURATION = deepFreeze([
|
|
44
|
+
{ key: "name", label: "단서 이름", required: true, description: "단서를 구분하는 표시 이름" },
|
|
45
|
+
{ key: "title", label: "단서 제목", required: true, description: "단서 카드에 표시하는 제목" },
|
|
46
|
+
{ key: "description", label: "설명", required: true, description: "플레이어가 읽는 단서 내용" },
|
|
47
|
+
{ key: "confirmed", label: "확정 여부", description: "확정된 단서만 미리보기에 표시" },
|
|
48
|
+
{ key: "image", label: "이미지", description: "단서 이미지 등록과 미리보기" },
|
|
49
|
+
{ key: "location", label: "위치", description: "단서를 발견하거나 사용하는 위치" },
|
|
50
|
+
{ key: "tags", label: "태그", description: "단서 분류와 미리보기 필터" },
|
|
51
|
+
{ key: "color", label: "컬러", description: "단서 분류용 대표 색상" },
|
|
52
|
+
{ key: "secret", label: "작가 노트", description: "제작자만 확인하는 단서 메모" }
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const BASIC_FIELD_CONFIGURATION = deepFreeze([
|
|
56
|
+
{ key: "title", label: "작품 제목", required: true, description: "작품을 구분하고 상세 화면에 표시하는 제목" },
|
|
57
|
+
{ key: "playerCount", label: "플레이 인원", required: true, description: "최소·최대 플레이 인원" },
|
|
58
|
+
{ key: "intro", label: "2줄 소개", required: true, description: "작품을 짧게 소개하는 문구" },
|
|
59
|
+
{ key: "detail", label: "상세 페이지", required: true, description: "작품의 상세 소개와 안내" },
|
|
60
|
+
{ key: "theme", label: "화면 모드", required: true, description: "라이트 또는 다크 화면 설정" },
|
|
61
|
+
{ key: "font", label: "폰트", required: true, description: "워크벤치와 미리보기에 사용할 글꼴" },
|
|
62
|
+
{ key: "poster", label: "포스터 이미지", required: true, description: "작품 상세 화면의 대표 이미지" },
|
|
63
|
+
{ key: "logo", label: "로고 이미지", required: true, description: "작품 상세 화면의 로고" },
|
|
64
|
+
{ key: "workspace-background", label: "배경 이미지", required: true, description: "제작 화면과 미리보기의 배경 이미지" },
|
|
65
|
+
{ key: "brandColor", label: "브랜드 컬러", required: true, description: "작품의 강조 영역에 사용하는 색상" },
|
|
66
|
+
{ key: "backgroundColor", label: "배경 컬러", required: true, description: "제작 화면과 미리보기의 기본 배경색" }
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const CHARACTER_FIELD_CONFIGURATION = deepFreeze([
|
|
70
|
+
{ key: "name", label: "캐릭터 이름", required: true, description: "캐릭터를 구분하는 표시 이름" },
|
|
71
|
+
{ key: "isPlayable", label: "플레이어 캐릭터", required: true, description: "플레이어가 맡는 캐릭터인지 NPC인지 구분" },
|
|
72
|
+
{ key: "customSections", label: "캐릭터 원고", required: true, description: "Container에 자유롭게 배치하는 제목과 본문" },
|
|
73
|
+
{ key: "containers", label: "원고 Container", required: true, description: "캐릭터 기본정보와 원고 section의 묶음·순서" },
|
|
74
|
+
{ key: "image", label: "캐릭터 이미지", required: true, description: "캐릭터 이미지 등록과 미리보기" },
|
|
75
|
+
{ key: "color", label: "컬러", required: true, description: "캐릭터 카드의 대표 색상" },
|
|
76
|
+
{ key: "tag", label: "태그", required: true, description: "캐릭터의 역할과 분류 태그" },
|
|
77
|
+
{ key: "authorNote", label: "작가 노트", description: "연기 방향과 설계 의도 메모" }
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
function template(input) {
|
|
81
|
+
return {
|
|
82
|
+
templateId: input.templateId,
|
|
83
|
+
defaultInstanceId: input.defaultInstanceId || input.templateId.split(".").at(-1),
|
|
84
|
+
name: input.name,
|
|
85
|
+
description: input.description || "",
|
|
86
|
+
dataSchema: fieldContracts.jsonSchemaFor(input.templateId),
|
|
87
|
+
required: input.required === true,
|
|
88
|
+
removable: input.required !== true,
|
|
89
|
+
allowMultiple: input.allowMultiple === true,
|
|
90
|
+
internal: input.internal === true,
|
|
91
|
+
guide: clonePlain(input.guide || {}),
|
|
92
|
+
fieldConfiguration: input.fieldConfiguration || [],
|
|
93
|
+
views: {
|
|
94
|
+
[WORKSPACE_MODES.AUTHORING]: input.authoring || directAuthoring,
|
|
95
|
+
[WORKSPACE_MODES.PREVIEW]: input.preview || rawPreview,
|
|
96
|
+
[WORKSPACE_MODES.DESIGN]: input.design || genericDesignViews,
|
|
97
|
+
[WORKSPACE_MODES.VALIDATION]: input.validation || genericValidationViews
|
|
98
|
+
},
|
|
99
|
+
defaultViews: {
|
|
100
|
+
[WORKSPACE_MODES.AUTHORING]: (input.authoring || directAuthoring)[0]?.id || null,
|
|
101
|
+
[WORKSPACE_MODES.PREVIEW]: (input.preview || rawPreview)[0]?.id || null,
|
|
102
|
+
[WORKSPACE_MODES.DESIGN]: (input.design || genericDesignViews)[0]?.id || null,
|
|
103
|
+
[WORKSPACE_MODES.VALIDATION]: (input.validation || genericValidationViews)[0]?.id || null
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const FIELD_CONFIGURATION = Object.freeze({
|
|
109
|
+
"ssobig.basic": BASIC_FIELD_CONFIGURATION,
|
|
110
|
+
"ssobig.character": CHARACTER_FIELD_CONFIGURATION,
|
|
111
|
+
"ssobig.clues": CLUE_FIELD_CONFIGURATION
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const INITIAL_TEMPLATE_CATALOG = deepFreeze(catalogContract.list().map(descriptor => template({
|
|
115
|
+
templateId: descriptor.templateId,
|
|
116
|
+
name: descriptor.tabLabel,
|
|
117
|
+
required: descriptor.required,
|
|
118
|
+
internal: descriptor.internal,
|
|
119
|
+
allowMultiple: descriptor.allowMultiple,
|
|
120
|
+
defaultInstanceId: descriptor.defaultInstanceId,
|
|
121
|
+
description: descriptor.description,
|
|
122
|
+
guide: descriptor.guide,
|
|
123
|
+
fieldConfiguration: FIELD_CONFIGURATION[descriptor.templateId] || []
|
|
124
|
+
})));
|
|
125
|
+
|
|
126
|
+
function normalizeTemplate(input) {
|
|
127
|
+
if (!input || typeof input !== "object") throw new TypeError("Component template must be an object");
|
|
128
|
+
const templateId = String(input.templateId || "").trim();
|
|
129
|
+
if (!/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/.test(templateId)) throw new TypeError(`Invalid component template ID: ${templateId}`);
|
|
130
|
+
const schema = input.dataSchema || fieldContracts.jsonSchemaFor(templateId);
|
|
131
|
+
if (!schema) throw new TypeError(`Component data schema is not registered: ${templateId}`);
|
|
132
|
+
const views = {};
|
|
133
|
+
const defaultViews = {};
|
|
134
|
+
[WORKSPACE_MODES.AUTHORING, WORKSPACE_MODES.PREVIEW, WORKSPACE_MODES.DESIGN, WORKSPACE_MODES.VALIDATION].forEach(mode => {
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
views[mode] = (Array.isArray(input.views?.[mode]) ? input.views[mode] : []).map(item => {
|
|
137
|
+
const source = typeof item === "string" ? { id: item, label: item } : item || {};
|
|
138
|
+
const id = assertVariantId(normalizeVariantId(source.id, null), `${templateId}.${mode}.view.id`);
|
|
139
|
+
if (seen.has(id)) throw new Error(`Duplicate ${mode} view '${id}' in ${templateId}`);
|
|
140
|
+
seen.add(id);
|
|
141
|
+
return { id, label: String(source.label || id), kind: String(source.kind || id), implemented: source.implemented !== false, description: String(source.description || "") };
|
|
142
|
+
});
|
|
143
|
+
const requestedDefault = input.defaultViews?.[mode];
|
|
144
|
+
const defaultId = requestedDefault == null ? views[mode][0]?.id || null : normalizeVariantId(requestedDefault, null);
|
|
145
|
+
if (defaultId && !seen.has(defaultId)) throw new Error(`Default ${mode} view '${defaultId}' is not registered in ${templateId}`);
|
|
146
|
+
defaultViews[mode] = defaultId;
|
|
147
|
+
});
|
|
148
|
+
const defaultInstanceId = String(input.defaultInstanceId || templateId.split(".").at(-1));
|
|
149
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(defaultInstanceId)) throw new TypeError(`Invalid default instance ID for ${templateId}`);
|
|
150
|
+
return deepFreeze({
|
|
151
|
+
templateId,
|
|
152
|
+
defaultInstanceId,
|
|
153
|
+
name: String(input.name || templateId),
|
|
154
|
+
description: String(input.description || ""),
|
|
155
|
+
dataSchema: clonePlain(schema),
|
|
156
|
+
required: input.required === true,
|
|
157
|
+
removable: input.required === true ? false : input.removable !== false,
|
|
158
|
+
allowMultiple: input.allowMultiple === true,
|
|
159
|
+
internal: input.internal === true,
|
|
160
|
+
guide: clonePlain(input.guide || {}),
|
|
161
|
+
fieldConfiguration: (Array.isArray(input.fieldConfiguration) ? input.fieldConfiguration : []).map(item => ({
|
|
162
|
+
key: String(item?.key || ""), label: String(item?.label || item?.key || ""), description: String(item?.description || ""), required: item?.required === true
|
|
163
|
+
})),
|
|
164
|
+
views,
|
|
165
|
+
defaultViews
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createRegistry(initialTemplates = INITIAL_TEMPLATE_CATALOG) {
|
|
170
|
+
const templates = new Map();
|
|
171
|
+
|
|
172
|
+
function register(input, options = {}) {
|
|
173
|
+
const normalized = normalizeTemplate(input);
|
|
174
|
+
if (templates.has(normalized.templateId) && options.replace !== true) throw new Error(`Component template already registered: ${normalized.templateId}`);
|
|
175
|
+
templates.set(normalized.templateId, normalized);
|
|
176
|
+
return normalized;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function get(templateId) {
|
|
180
|
+
return templates.get(String(templateId || "").trim()) || null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function canInstantiate(templateId, instances = []) {
|
|
184
|
+
const selected = get(templateId);
|
|
185
|
+
if (!selected) return false;
|
|
186
|
+
return selected.allowMultiple || !instances.some(instance => instance?.templateId === selected.templateId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
initialTemplates.forEach(item => register(item));
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
register,
|
|
192
|
+
get,
|
|
193
|
+
has: id => Boolean(get(id)),
|
|
194
|
+
canInstantiate,
|
|
195
|
+
list: () => [...templates.values()].sort((left, right) => left.templateId.localeCompare(right.templateId))
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
INITIAL_TEMPLATE_CATALOG,
|
|
201
|
+
normalizeTemplate,
|
|
202
|
+
createRegistry,
|
|
203
|
+
createDefaultRegistry: () => createRegistry(INITIAL_TEMPLATE_CATALOG)
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
let dependencies = root ? {
|
|
3
|
+
appearance: root.WriterWorkbenchAppearance,
|
|
4
|
+
shared: root.WriterWorkbenchRendererShared,
|
|
5
|
+
basic: root.WriterWorkbenchBasicRenderer,
|
|
6
|
+
common: root.WriterWorkbenchCommonRenderer,
|
|
7
|
+
character: root.WriterWorkbenchCharacterRenderer,
|
|
8
|
+
clues: root.WriterWorkbenchCluesRenderer,
|
|
9
|
+
clueCombinations: root.WriterWorkbenchClueCombinationsRenderer,
|
|
10
|
+
aiResponse: root.WriterWorkbenchAiResponseRenderer,
|
|
11
|
+
timeline: root.WriterWorkbenchTimelineRenderer,
|
|
12
|
+
ending: root.WriterWorkbenchEndingRenderer,
|
|
13
|
+
postgame: root.WriterWorkbenchPostgameRenderer,
|
|
14
|
+
authorNotes: root.WriterWorkbenchAuthorNotesRenderer,
|
|
15
|
+
investigationBoard: root.WriterWorkbenchInvestigationBoardRenderer,
|
|
16
|
+
documents: root.WriterWorkbenchDocumentRenderers
|
|
17
|
+
} : {};
|
|
18
|
+
if (typeof module === "object" && module.exports) dependencies = {
|
|
19
|
+
appearance: require("./renderers/appearance.js"),
|
|
20
|
+
shared: require("./renderers/shared.js"),
|
|
21
|
+
basic: require("./renderers/basic.js"),
|
|
22
|
+
common: require("./renderers/common.js"),
|
|
23
|
+
character: require("./renderers/character.js"),
|
|
24
|
+
clues: require("./renderers/clues.js"),
|
|
25
|
+
clueCombinations: require("./renderers/clue-combinations.js"),
|
|
26
|
+
aiResponse: require("./renderers/ai-response.js"),
|
|
27
|
+
timeline: require("./renderers/timeline.js"),
|
|
28
|
+
ending: require("./renderers/ending.js"),
|
|
29
|
+
postgame: require("./renderers/postgame.js"),
|
|
30
|
+
authorNotes: require("./renderers/author-notes.js"),
|
|
31
|
+
investigationBoard: require("./renderers/investigation-board.js"),
|
|
32
|
+
documents: require("./renderers/documents.js")
|
|
33
|
+
};
|
|
34
|
+
const api = factory(dependencies);
|
|
35
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
36
|
+
if (root) root.WriterWorkbenchComponentRenderers = api;
|
|
37
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (dependencies) {
|
|
38
|
+
"use strict";
|
|
39
|
+
const { appearance, shared, basic, common, character, clues, clueCombinations, aiResponse, timeline, ending, postgame, authorNotes, investigationBoard, documents } = dependencies;
|
|
40
|
+
if (!appearance || !shared || !basic || !common || !character || !clues || !clueCombinations || !aiResponse || !timeline || !ending || !postgame || !authorNotes || !investigationBoard || !documents) throw new Error("Component renderer modules are incomplete");
|
|
41
|
+
|
|
42
|
+
const renderers = new Map();
|
|
43
|
+
function register(renderer) {
|
|
44
|
+
const key = String(renderer.templateId || "");
|
|
45
|
+
if (renderers.has(key)) throw new Error(`중복 Component 렌더러입니다: ${key}`);
|
|
46
|
+
renderers.set(key, Object.freeze({ ...renderer }));
|
|
47
|
+
}
|
|
48
|
+
[basic, common, { ...common, templateId: "ssobig.choice" }, character, clues, clueCombinations, aiResponse, timeline, ending, postgame, authorNotes, investigationBoard].forEach(register);
|
|
49
|
+
Object.entries(documents.definitions).forEach(([templateId, definition]) => register({
|
|
50
|
+
templateId,
|
|
51
|
+
renderAuthoring: documents.renderAuthoring,
|
|
52
|
+
renderPreview: documents.renderPreview,
|
|
53
|
+
bind: documents.bind
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
function resolve(instance) {
|
|
57
|
+
const key = String(instance?.templateId || "");
|
|
58
|
+
const renderer = renderers.get(key);
|
|
59
|
+
if (!renderer) throw new Error(`지원되지 않는 Component 렌더러입니다: ${key}`);
|
|
60
|
+
return renderer;
|
|
61
|
+
}
|
|
62
|
+
function resolveView(viewModel) {
|
|
63
|
+
const view = viewModel?.view;
|
|
64
|
+
const primary = viewModel?.primary;
|
|
65
|
+
if (!view || !primary?.instance || view.rendererId !== primary.instance.templateId) {
|
|
66
|
+
throw new Error("View Component renderer binding이 올바르지 않습니다.");
|
|
67
|
+
}
|
|
68
|
+
const key = view.rendererId;
|
|
69
|
+
const renderer = renderers.get(key);
|
|
70
|
+
if (!renderer) throw new Error(`지원되지 않는 View Component 렌더러입니다: ${key}`);
|
|
71
|
+
return renderer;
|
|
72
|
+
}
|
|
73
|
+
function defaultView() { return "preview"; }
|
|
74
|
+
function context(instance, data, extra = {}) {
|
|
75
|
+
return { instance, data, ...extra, view: shared.rendererView(extra, defaultView(instance)) };
|
|
76
|
+
}
|
|
77
|
+
function viewContext(viewModel, extra = {}) {
|
|
78
|
+
const primary = viewModel?.primary;
|
|
79
|
+
return {
|
|
80
|
+
...extra,
|
|
81
|
+
viewComponent: viewModel.view,
|
|
82
|
+
bindings: viewModel.bindings,
|
|
83
|
+
instance: primary.instance,
|
|
84
|
+
data: primary.data,
|
|
85
|
+
view: shared.rendererView(extra, defaultView(primary.instance))
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function applyRendererView(markup, view) {
|
|
89
|
+
return String(markup)
|
|
90
|
+
.replace(/data-renderer-view="(?:input|preview)"/, `data-renderer-view="${view}"`);
|
|
91
|
+
}
|
|
92
|
+
function renderAuthoring(instance, data, extra) {
|
|
93
|
+
const rendererContext = context(instance, data, extra);
|
|
94
|
+
return applyRendererView(resolve(instance).renderAuthoring(rendererContext), rendererContext.view);
|
|
95
|
+
}
|
|
96
|
+
function renderPreview(instance, data = instance?.data, extra) { return resolve(instance).renderPreview(context(instance, data, extra)); }
|
|
97
|
+
function bind(instance, root, data, extra) {
|
|
98
|
+
const renderer = resolve(instance);
|
|
99
|
+
const rendererContext = context(instance, data, extra);
|
|
100
|
+
rendererContext.renderPreview = () => renderer.renderPreview(rendererContext);
|
|
101
|
+
return typeof renderer.bind === "function" ? renderer.bind(root, rendererContext) : () => {};
|
|
102
|
+
}
|
|
103
|
+
function renderAuthoringView(viewModel, extra) {
|
|
104
|
+
const renderer = resolveView(viewModel);
|
|
105
|
+
const rendererContext = viewContext(viewModel, extra);
|
|
106
|
+
return applyRendererView(renderer.renderAuthoring(rendererContext), rendererContext.view);
|
|
107
|
+
}
|
|
108
|
+
function renderPreviewView(viewModel, extra) {
|
|
109
|
+
return resolveView(viewModel).renderPreview(viewContext(viewModel, extra));
|
|
110
|
+
}
|
|
111
|
+
function bindView(viewModel, root, extra) {
|
|
112
|
+
const renderer = resolveView(viewModel);
|
|
113
|
+
const rendererContext = viewContext(viewModel, extra);
|
|
114
|
+
rendererContext.renderPreview = () => renderer.renderPreview(rendererContext);
|
|
115
|
+
return typeof renderer.bind === "function" ? renderer.bind(root, rendererContext) : () => {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
resolve,
|
|
120
|
+
resolveView,
|
|
121
|
+
defaultView,
|
|
122
|
+
renderAuthoring,
|
|
123
|
+
renderPreview,
|
|
124
|
+
bind,
|
|
125
|
+
renderAuthoringView,
|
|
126
|
+
renderPreviewView,
|
|
127
|
+
bindView,
|
|
128
|
+
list: () => [...renderers.keys()],
|
|
129
|
+
clone: shared.clone,
|
|
130
|
+
inlineText: shared.inlineText,
|
|
131
|
+
formattedText: shared.formattedText,
|
|
132
|
+
pathToken: shared.pathToken,
|
|
133
|
+
decodePath: shared.decodePath,
|
|
134
|
+
escapeHtml: shared.escapeHtml,
|
|
135
|
+
clueAssetId: clues.clueAssetId,
|
|
136
|
+
setProgressPreviewMode: documents.setProgressPreviewMode,
|
|
137
|
+
appearance
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
let fieldContracts = root && root.WriterWorkbenchComponentFieldContracts;
|
|
3
|
+
let catalogContract = root && root.WriterWorkbenchComponentCatalogContract;
|
|
4
|
+
if (typeof module === "object" && module.exports) {
|
|
5
|
+
fieldContracts = require("./component-field-contracts.js");
|
|
6
|
+
catalogContract = require("./component-catalog-contract.js");
|
|
7
|
+
}
|
|
8
|
+
const api = factory(fieldContracts, catalogContract);
|
|
9
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
10
|
+
if (root) root.WriterWorkbenchComponentStorageContract = api;
|
|
11
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (fieldContracts, catalogContract) {
|
|
12
|
+
"use strict";
|
|
13
|
+
if (!fieldContracts) throw new Error("WriterWorkbenchComponentFieldContracts is required");
|
|
14
|
+
if (!catalogContract) throw new Error("WriterWorkbenchComponentCatalogContract is required");
|
|
15
|
+
|
|
16
|
+
const MANIFEST_SCHEMA_VERSION = "project-component-manifest-v1";
|
|
17
|
+
const COMPONENT_SPECS = Object.freeze(catalogContract.list().map(descriptor => spec(descriptor.templateId, descriptor)));
|
|
18
|
+
const COMPONENT_BY_ID = new Map(COMPONENT_SPECS.map(component => [component.templateId, component]));
|
|
19
|
+
const REQUIRED_COMPONENTS = Object.freeze(COMPONENT_SPECS.filter(component => component.required));
|
|
20
|
+
const REMOVED_VERSION_FIELDS = Object.freeze(["template_version", "state_schema_version", "templateVersion", "stateSchemaVersion"]);
|
|
21
|
+
|
|
22
|
+
function spec(templateId, options = {}) {
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
templateId,
|
|
25
|
+
required: options.required === true,
|
|
26
|
+
internal: options.internal === true,
|
|
27
|
+
allowMultiple: options.allowMultiple === true,
|
|
28
|
+
editorViews: Object.freeze(["json"]),
|
|
29
|
+
previewViews: Object.freeze(["raw"]),
|
|
30
|
+
validate: data => fieldContracts.validateTargetData(templateId, data)
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isObject(value) { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }
|
|
35
|
+
function clone(value) {
|
|
36
|
+
if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
|
|
37
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
38
|
+
if (isObject(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clone(item)]));
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function failure(code, message) { const error = new Error(message); error.code = code; return { ok: false, error }; }
|
|
42
|
+
function contractError(code, message) { const error = new Error(message); error.code = code; return error; }
|
|
43
|
+
function componentSpec(templateId) { return COMPONENT_BY_ID.get(String(templateId || "")) || null; }
|
|
44
|
+
|
|
45
|
+
function containsAssetPointerField(value) {
|
|
46
|
+
if (Array.isArray(value)) return value.some(containsAssetPointerField);
|
|
47
|
+
if (!isObject(value)) return false;
|
|
48
|
+
return Object.entries(value).some(([key, item]) => {
|
|
49
|
+
const normalizedKey = key.replace(/[_-]/g, "").toLowerCase();
|
|
50
|
+
if (["image", "imageurl", "imageurls", "imagepath", "imagepaths", "imagefilename", "filename", "filepath", "fileurl"].some(suffix => normalizedKey.endsWith(suffix))) return true;
|
|
51
|
+
return containsAssetPointerField(item);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateComponentData(component, data) {
|
|
56
|
+
return component.validate(data)
|
|
57
|
+
|| (!component.internal && containsAssetPointerField(data) ? "파일 pointer는 ssobig.assets Component만 소유할 수 있습니다." : null);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeViewId(value) { return value == null ? null : typeof value === "string" && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value) ? value : null; }
|
|
61
|
+
function normalizeViewSelection(capabilities, editorViewId, previewViewId) {
|
|
62
|
+
const views = isObject(capabilities?.viewSelection) ? clone(capabilities.viewSelection) : {};
|
|
63
|
+
if (editorViewId !== null) views.authoring = editorViewId;
|
|
64
|
+
if (previewViewId !== null) views.preview = previewViewId;
|
|
65
|
+
return views;
|
|
66
|
+
}
|
|
67
|
+
function validViewSelection(value) {
|
|
68
|
+
return isObject(value) && Object.entries(value).every(([mode, viewId]) => ["authoring", "preview", "design", "validation"].includes(mode)
|
|
69
|
+
&& (viewId === null || (typeof viewId === "string" && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(viewId))));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeInstance(instance) {
|
|
73
|
+
if (!isObject(instance)
|
|
74
|
+
|| REMOVED_VERSION_FIELDS.some(field => Object.hasOwn(instance, field))
|
|
75
|
+
|| typeof instance.instance_id !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(instance.instance_id)
|
|
76
|
+
|| typeof instance.template_id !== "string" || !/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/.test(instance.template_id)
|
|
77
|
+
|| !Number.isSafeInteger(instance.sort_order) || instance.sort_order < 0
|
|
78
|
+
|| !Number.isSafeInteger(instance.revision) || instance.revision < 1
|
|
79
|
+
|| typeof instance.tab_label !== "string" || !instance.tab_label.trim() || instance.tab_label.length > 120
|
|
80
|
+
|| typeof instance.is_enabled !== "boolean" || typeof instance.is_archived !== "boolean"
|
|
81
|
+
|| typeof instance.is_required !== "boolean" || typeof instance.is_removable !== "boolean"
|
|
82
|
+
|| !isObject(instance.capabilities) || !isObject(instance.config) || !isObject(instance.data)) return null;
|
|
83
|
+
if (!componentSpec(instance.template_id)) return null;
|
|
84
|
+
const editorViewId = normalizeViewId(instance.editor_view_id);
|
|
85
|
+
const previewViewId = normalizeViewId(instance.preview_view_id);
|
|
86
|
+
if ((instance.editor_view_id != null && editorViewId === null) || (instance.preview_view_id != null && previewViewId === null)
|
|
87
|
+
|| (Object.hasOwn(instance.capabilities, "viewSelection") && !validViewSelection(instance.capabilities.viewSelection))) return null;
|
|
88
|
+
const capabilities = clone(instance.capabilities);
|
|
89
|
+
return {
|
|
90
|
+
instanceId: instance.instance_id,
|
|
91
|
+
templateId: instance.template_id,
|
|
92
|
+
tab: instance.tab_label,
|
|
93
|
+
order: instance.sort_order,
|
|
94
|
+
enabled: instance.is_enabled,
|
|
95
|
+
archived: instance.is_archived,
|
|
96
|
+
required: instance.is_required,
|
|
97
|
+
removable: instance.is_removable,
|
|
98
|
+
editorViewId,
|
|
99
|
+
previewViewId,
|
|
100
|
+
capabilities,
|
|
101
|
+
viewSelection: normalizeViewSelection(capabilities, editorViewId, previewViewId),
|
|
102
|
+
config: clone(instance.config),
|
|
103
|
+
data: clone(instance.data),
|
|
104
|
+
revision: instance.revision
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runtimeInstance(instance) { return instance.enabled && !instance.archived; }
|
|
109
|
+
function validateViewId(value, allowed, label) { return value === null || allowed.includes(value) ? null : `${label} 보기(${value})는 이 Component에서 지원되지 않습니다.`; }
|
|
110
|
+
function validateRuntimeInstance(instance) {
|
|
111
|
+
const component = componentSpec(instance.templateId);
|
|
112
|
+
if (!component) return `지원되지 않는 Component(${instance.templateId})입니다.`;
|
|
113
|
+
const configKeys = Object.keys(instance.config);
|
|
114
|
+
if (component.internal ? configKeys.some(key => key !== "hidden") : configKeys.length > 0) return `${instance.templateId} config에 지원되지 않는 필드가 있습니다.`;
|
|
115
|
+
const editorError = validateViewId(instance.editorViewId, component.editorViews, "편집");
|
|
116
|
+
const previewError = validateViewId(instance.previewViewId, component.previewViews, "미리보기");
|
|
117
|
+
const selectedAuthoringError = validateViewId(instance.viewSelection.authoring ?? null, component.editorViews, "저장된 편집");
|
|
118
|
+
const selectedPreviewError = validateViewId(instance.viewSelection.preview ?? null, component.previewViews, "저장된 미리보기");
|
|
119
|
+
const designError = validateViewId(instance.viewSelection.design ?? null, ["theme"], "저장된 디자인");
|
|
120
|
+
const validationError = validateViewId(instance.viewSelection.validation ?? null, ["schema"], "저장된 검증");
|
|
121
|
+
return editorError || previewError || selectedAuthoringError || selectedPreviewError || designError || validationError
|
|
122
|
+
|| validateComponentData(component, instance.data)
|
|
123
|
+
|| (component.internal && instance.config?.hidden !== true ? "에셋 Component는 내부 전용으로 숨김 처리되어야 합니다." : null);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compositionRelationError(instances) {
|
|
127
|
+
const character = instances.find(instance => instance.templateId === "ssobig.character");
|
|
128
|
+
if (character) {
|
|
129
|
+
const characterIds = new Set(Object.keys(character.data.characters || {}));
|
|
130
|
+
for (const timeline of instances.filter(instance => instance.templateId === "ssobig.timeline")) {
|
|
131
|
+
for (const event of timeline.data.events) {
|
|
132
|
+
const missing = Object.keys(event.memories).find(id => !characterIds.has(id));
|
|
133
|
+
if (missing) return `${timeline.instanceId} timeline memory가 존재하지 않는 character(${missing})를 가리킵니다.`;
|
|
134
|
+
const unassigned = Object.keys(event.unassignedMemories || {});
|
|
135
|
+
const assigned = unassigned.find(id => characterIds.has(id));
|
|
136
|
+
if (assigned) return `${timeline.instanceId} unassigned memory(${assigned})는 현재 character에 연결할 수 있습니다.`;
|
|
137
|
+
const overlap = unassigned.find(id => Object.hasOwn(event.memories, id));
|
|
138
|
+
if (overlap) return `${timeline.instanceId} memory(${overlap})가 assigned와 unassigned에 중복 저장되어 있습니다.`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const clues = instances.find(instance => instance.templateId === "ssobig.clues");
|
|
143
|
+
for (const combinations of instances.filter(instance => instance.templateId === "ssobig.clue-combinations")) {
|
|
144
|
+
if (!clues) return `${combinations.instanceId} 단서 조합에는 ssobig.clues 문맥이 필요합니다.`;
|
|
145
|
+
const clueIds = new Set(clues.data.clues.map(clue => String(clue.id).toLowerCase()));
|
|
146
|
+
for (const combination of combinations.data.combinations) {
|
|
147
|
+
const missing = [...combination.conditions, ...combination.results]
|
|
148
|
+
.filter(item => item.kind === "clue")
|
|
149
|
+
.map(item => String(item.clueId).toLowerCase())
|
|
150
|
+
.find(id => !clueIds.has(id));
|
|
151
|
+
if (missing) return `${combinations.instanceId} 단서 조합이 존재하지 않는 clue(${missing})를 가리킵니다.`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateActiveComponentSet(componentSet) {
|
|
158
|
+
if (String(componentSet?.canonical_status || "") !== "active") return failure("SOMI_MIGRATION_REQUIRED", "이 작품은 Component 이관이 아직 완료되지 않았습니다.");
|
|
159
|
+
if (!Number.isSafeInteger(componentSet?.version_number) || componentSet.version_number < 1 || !Number.isSafeInteger(componentSet?.revision) || componentSet.revision < 1) return failure("SOMI_MIGRATION_REQUIRED", "활성 Component Set 식별자나 revision이 올바르지 않습니다.");
|
|
160
|
+
if (!String(componentSet?.component_checksum || "").trim()) return failure("SOMI_MIGRATION_REQUIRED", "활성 Component 구성의 검증값이 없습니다. 이관 검증이 필요합니다.");
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function validRuntimeInstanceShape(instance) {
|
|
164
|
+
return isObject(instance)
|
|
165
|
+
&& REMOVED_VERSION_FIELDS.every(field => !Object.hasOwn(instance, field))
|
|
166
|
+
&& typeof instance.instanceId === "string" && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(instance.instanceId)
|
|
167
|
+
&& typeof instance.templateId === "string" && /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/.test(instance.templateId)
|
|
168
|
+
&& Number.isSafeInteger(instance.order) && instance.order >= 0
|
|
169
|
+
&& Number.isSafeInteger(instance.revision) && instance.revision >= 1
|
|
170
|
+
&& typeof instance.tab === "string" && Boolean(instance.tab.trim()) && instance.tab.length <= 120
|
|
171
|
+
&& typeof instance.enabled === "boolean" && typeof instance.archived === "boolean"
|
|
172
|
+
&& typeof instance.required === "boolean" && typeof instance.removable === "boolean"
|
|
173
|
+
&& isObject(instance.capabilities) && isObject(instance.viewSelection) && isObject(instance.config) && isObject(instance.data);
|
|
174
|
+
}
|
|
175
|
+
function validateRuntimeComposition(componentSet, instances) {
|
|
176
|
+
const setError = validateActiveComponentSet(componentSet);
|
|
177
|
+
if (setError) return setError;
|
|
178
|
+
if (!Array.isArray(instances) || !instances.length) return failure("SOMI_MIGRATION_REQUIRED", "활성화된 Component가 없습니다. 이관 검증이 필요합니다.");
|
|
179
|
+
if (instances.some(instance => !validRuntimeInstanceShape(instance) || !runtimeInstance(instance))) return failure("SOMI_MIGRATION_REQUIRED", "활성 Component 런타임 데이터 형식이 완전하지 않습니다. 이관 검증이 필요합니다.");
|
|
180
|
+
const activeInstances = [...instances].sort((left, right) => left.order - right.order);
|
|
181
|
+
if (new Set(activeInstances.map(instance => instance.instanceId)).size !== activeInstances.length || new Set(activeInstances.map(instance => instance.order)).size !== activeInstances.length) return failure("SOMI_MIGRATION_REQUIRED", "활성 Component 식별자 또는 순서가 중복되었습니다.");
|
|
182
|
+
for (const instance of activeInstances) { const invalid = validateRuntimeInstance(instance); if (invalid) return failure("SOMI_MIGRATION_REQUIRED", `${invalid} 이관 검증이 필요합니다.`); }
|
|
183
|
+
const relationError = compositionRelationError(activeInstances);
|
|
184
|
+
if (relationError) return failure("SOMI_MIGRATION_REQUIRED", `${relationError} 이관 검증이 필요합니다.`);
|
|
185
|
+
for (const templateId of new Set(REQUIRED_COMPONENTS.map(component => component.templateId))) {
|
|
186
|
+
const matches = activeInstances.filter(instance => instance.templateId === templateId);
|
|
187
|
+
if (matches.length !== 1 || !matches[0].required || matches[0].removable !== false) return failure("SOMI_MIGRATION_REQUIRED", `필수 Component(${templateId})가 완전하지 않습니다. 이관 검증이 필요합니다.`);
|
|
188
|
+
}
|
|
189
|
+
const singletonTemplateIds = new Set(COMPONENT_SPECS.filter(item => !item.allowMultiple).map(item => item.templateId));
|
|
190
|
+
for (const templateId of singletonTemplateIds) if (activeInstances.filter(instance => instance.templateId === templateId).length > 1) return failure("SOMI_MIGRATION_REQUIRED", `${templateId} Component가 중복되어 있습니다. 이관 검증이 필요합니다.`);
|
|
191
|
+
return { ok: true, instances: activeInstances };
|
|
192
|
+
}
|
|
193
|
+
function validateActiveComposition(componentSet, rows) {
|
|
194
|
+
const setError = validateActiveComponentSet(componentSet);
|
|
195
|
+
if (setError) return setError;
|
|
196
|
+
if (!Array.isArray(rows) || !rows.length) return failure("SOMI_MIGRATION_REQUIRED", "저장된 Component가 없습니다. 이 작품은 이관이 필요합니다.");
|
|
197
|
+
if (rows.some(row => !isObject(row) || typeof row.is_enabled !== "boolean" || typeof row.is_archived !== "boolean")) return failure("SOMI_MIGRATION_REQUIRED", "Component 활성 상태가 완전하지 않습니다. 이관 검증이 필요합니다.");
|
|
198
|
+
const activeRows = rows.filter(row => row.is_enabled && !row.is_archived);
|
|
199
|
+
const instances = activeRows.map(normalizeInstance).filter(Boolean).sort((left, right) => left.order - right.order);
|
|
200
|
+
if (instances.length !== activeRows.length) return failure("SOMI_MIGRATION_REQUIRED", "활성 Component 데이터 형식이 완전하지 않습니다. 이관 검증이 필요합니다.");
|
|
201
|
+
return validateRuntimeComposition(componentSet, instances);
|
|
202
|
+
}
|
|
203
|
+
function archivedInstances(rows) {
|
|
204
|
+
if (!Array.isArray(rows)) return [];
|
|
205
|
+
return rows.filter(row => row?.is_archived === true || row?.is_enabled === false)
|
|
206
|
+
.map(normalizeInstance)
|
|
207
|
+
.filter(instance => instance && !instance.required && instance.removable && validateRuntimeInstance(instance) === null)
|
|
208
|
+
.sort((left, right) => left.order - right.order);
|
|
209
|
+
}
|
|
210
|
+
function manifest(project, instances, views = []) {
|
|
211
|
+
const title = String(instances.find(instance => runtimeInstance(instance) && instance.templateId === "ssobig.basic")?.data?.title || "");
|
|
212
|
+
return {
|
|
213
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
214
|
+
project: { id: String(project?.id || ""), title },
|
|
215
|
+
components: instances.map(instance => ({ instanceId: instance.instanceId, templateId: instance.templateId, tab: instance.tab, order: instance.order, enabled: instance.enabled, archived: instance.archived, required: instance.required, removable: instance.removable, editorViewId: instance.editorViewId, previewViewId: instance.previewViewId, viewSelection: clone(instance.viewSelection), capabilities: clone(instance.capabilities), config: clone(instance.config) })),
|
|
216
|
+
views: views.map(view => ({ viewInstanceId: view.viewInstanceId, templateId: view.templateId, label: view.label, rendererId: view.rendererId, modes: clone(view.modes), order: view.order, enabled: view.enabled, config: clone(view.config), bindings: clone(view.bindings) }))
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function runtimeSnapshot(project, componentSet, instances, options = {}) {
|
|
220
|
+
const title = String(instances.find(instance => runtimeInstance(instance) && instance.templateId === "ssobig.basic")?.data?.title || "");
|
|
221
|
+
const archived = Array.isArray(options.archivedInstances) ? options.archivedInstances : [];
|
|
222
|
+
return { schemaVersion: "ssobig-writer-component-runtime-snapshot-v3", project: { id: String(project?.id || ""), slug: String(project?.slug || ""), title, authorName: String(project?.author_name || project?.authorName || ""), storagePrefix: String(options.storagePrefix || "") }, componentSet: { versionNumber: Number(componentSet?.version_number || 0), revision: Number(componentSet?.revision || 0), checksum: String(componentSet?.component_checksum || "") }, dataComponents: instances.map(instance => ({ ...instance, capabilities: clone(instance.capabilities), viewSelection: clone(instance.viewSelection), config: clone(instance.config), data: clone(instance.data) })), archivedDataComponents: archived.map(instance => ({ ...instance, capabilities: clone(instance.capabilities), viewSelection: clone(instance.viewSelection), config: clone(instance.config), data: clone(instance.data) })), viewComponents: [] };
|
|
223
|
+
}
|
|
224
|
+
function compositionPayload(instances) { return instances.map(instancePayload); }
|
|
225
|
+
function instancePayload(instance) {
|
|
226
|
+
const component = componentSpec(instance?.templateId);
|
|
227
|
+
if (!component || REMOVED_VERSION_FIELDS.some(field => Object.hasOwn(instance, field)) || validateComponentData(component, instance.data)) {
|
|
228
|
+
throw contractError("SOMI_INVALID_COMPONENT_INSTANCE", "유효한 직접 Component data만 저장할 수 있습니다.");
|
|
229
|
+
}
|
|
230
|
+
return { instance_id: instance.instanceId, template_id: instance.templateId, tab_label: instance.tab, sort_order: instance.order, is_enabled: instance.enabled, is_archived: instance.archived, is_required: instance.required, is_removable: instance.removable, editor_view_id: instance.editorViewId, preview_view_id: instance.previewViewId, capabilities: clone(instance.capabilities), config: clone(instance.config), data: clone(instance.data) };
|
|
231
|
+
}
|
|
232
|
+
function assetInstance(instances) { const assets = instances.filter(instance => runtimeInstance(instance) && instance.templateId === "ssobig.assets"); if (assets.length !== 1) throw contractError("SOMI_INVALID_ASSET_COMPONENT", "활성 에셋 Component가 정확히 하나여야 합니다."); return assets[0]; }
|
|
233
|
+
function assetManifest(instances) { return clone(assetInstance(instances).data); }
|
|
234
|
+
function assetInstanceWithManifest(instance, value, revision = instance?.revision) { if (!instance || instance.templateId !== "ssobig.assets" || componentSpec("ssobig.assets").validate(value)) throw contractError("SOMI_INVALID_ASSET_COMPONENT", "유효한 에셋 Component 데이터가 필요합니다."); return { ...instance, data: clone(value), revision: Number(revision) }; }
|
|
235
|
+
|
|
236
|
+
return Object.freeze({ MANIFEST_SCHEMA_VERSION, SUPPORTED_COMPONENTS: COMPONENT_SPECS, REQUIRED_COMPONENTS, componentSpec, normalizeInstance, runtimeInstance, validateActiveComposition, validateRuntimeComposition, archivedInstances, manifest, runtimeSnapshot, compositionPayload, instancePayload, assetInstance, assetManifest, assetInstanceWithManifest });
|
|
237
|
+
});
|