@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,103 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchComponentAssetOperations = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
function characterAssetId(characterId) {
|
|
9
|
+
return `character-${characterId}`.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function usages(components, dataFor, clueAssetId) {
|
|
13
|
+
const result = {
|
|
14
|
+
poster: ["기본정보 · 포스터 이미지"],
|
|
15
|
+
logo: ["기본정보 · 로고 이미지"],
|
|
16
|
+
"workspace-background": ["기본정보 · 워크벤치 배경"]
|
|
17
|
+
};
|
|
18
|
+
const active = Array.isArray(components) ? components : [];
|
|
19
|
+
const characterInstance = active.find(instance => instance.templateId === "ssobig.character");
|
|
20
|
+
const characterData = dataFor(characterInstance) || characterInstance?.data;
|
|
21
|
+
const deleted = new Set(Array.isArray(characterData?.deletedColumns) ? characterData.deletedColumns : []);
|
|
22
|
+
for (const characterId of Array.isArray(characterData?.order) ? characterData.order : []) {
|
|
23
|
+
if (deleted.has(characterId) || !Object.hasOwn(characterData?.characters || {}, characterId)) continue;
|
|
24
|
+
const assetId = characterAssetId(characterId);
|
|
25
|
+
if (!result[assetId]) result[assetId] = [];
|
|
26
|
+
result[assetId].push(`캐릭터 · ${characterData.names?.[characterId] || characterId}`);
|
|
27
|
+
}
|
|
28
|
+
const clueInstance = active.find(instance => instance.templateId === "ssobig.clues");
|
|
29
|
+
const clueData = dataFor(clueInstance) || clueInstance?.data;
|
|
30
|
+
for (const [index, clue] of (Array.isArray(clueData?.clues) ? clueData.clues : []).entries()) {
|
|
31
|
+
const assetId = clueAssetId(clue);
|
|
32
|
+
if (!assetId) continue;
|
|
33
|
+
if (!result[assetId]) result[assetId] = [];
|
|
34
|
+
const label = clue.name || clue.title || clue.id || index + 1;
|
|
35
|
+
result[assetId].push(`단서 · ${label}`);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function create(options) {
|
|
41
|
+
if (!options?.sync || typeof options?.clone !== "function" || typeof options?.draft !== "function" || typeof options?.clueAssetId !== "function") {
|
|
42
|
+
throw new Error("Component asset 작업 의존성이 완전하지 않습니다.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function currentManifest() {
|
|
46
|
+
return options.sync.getAssetManifest?.() || options.clone(options.getAssetInstance?.()?.data || { assets: {} });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function currentEntry(assetId) {
|
|
50
|
+
return options.sync.getAssetEntry?.(assetId) || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function currentUsages() {
|
|
54
|
+
return usages(options.activeComponents, instance => options.draft(instance), options.clueAssetId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function run(ownerInstance, operation) {
|
|
58
|
+
if (options.isReadOnly?.()) throw new Error("이 작품은 읽기 전용입니다.");
|
|
59
|
+
if (options.mutationLocked?.()) throw new Error("체크포인트 작업이 끝난 뒤 에셋을 변경해 주세요.");
|
|
60
|
+
if (ownerInstance && options.saveState(ownerInstance).locked) throw options.saveState(ownerInstance).error;
|
|
61
|
+
options.setStatus?.({ state: "saving", message: "에셋 파일을 저장하고 있습니다…" });
|
|
62
|
+
try {
|
|
63
|
+
const result = await operation();
|
|
64
|
+
if (ownerInstance) {
|
|
65
|
+
options.autosave.clearError(ownerInstance.instanceId);
|
|
66
|
+
options.updateStatus?.(ownerInstance);
|
|
67
|
+
}
|
|
68
|
+
options.setStatus?.({ state: "saved", message: "에셋이 저장되어 모든 연결 화면에 반영되었습니다." });
|
|
69
|
+
options.hydrate?.();
|
|
70
|
+
return result;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (ownerInstance) options.autosave.recordError(ownerInstance.instanceId, error);
|
|
73
|
+
options.setStatus?.({ state: "error", message: error?.message || "에셋을 저장하지 못했습니다." });
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function confirmRemoval(assetId) {
|
|
79
|
+
if (!currentEntry(assetId)) return false;
|
|
80
|
+
const linkedUsages = currentUsages()[assetId] || [];
|
|
81
|
+
const detail = linkedUsages.length ? `\n\n사용처: ${linkedUsages.join(", ")}\n연결된 화면에는 ‘선택된 파일 없음’으로 표시됩니다.` : "";
|
|
82
|
+
return options.confirm(`‘${assetId}’ 에셋 연결을 해제할까요?${detail}\n\n복제 작품 보호를 위해 기존 Storage 파일은 물리 삭제하지 않습니다.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function remove(ownerInstance, assetId) {
|
|
86
|
+
if (!confirmRemoval(assetId)) return Promise.resolve({ cancelled: true });
|
|
87
|
+
return run(ownerInstance, () => options.sync.deleteAsset(assetId));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function applyAppearance() {
|
|
91
|
+
const basicInstance = options.getBasicInstance?.();
|
|
92
|
+
if (!basicInstance) return null;
|
|
93
|
+
return options.appearance.apply(options.draft(basicInstance) || basicInstance.data, {
|
|
94
|
+
target: options.appearanceTarget,
|
|
95
|
+
backgroundImageUrl: options.sync.getAssetUrl?.(options.appearance.BACKGROUND_ASSET_ID) || ""
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Object.freeze({ currentManifest, currentEntry, usages: currentUsages, run, confirmRemoval, remove, applyAppearance });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Object.freeze({ characterAssetId, usages, create });
|
|
103
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchComponentAutosave = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
function create(options) {
|
|
9
|
+
const delay = Number(options.delay || 500);
|
|
10
|
+
const states = new Map();
|
|
11
|
+
let globalError = null;
|
|
12
|
+
function state(id) {
|
|
13
|
+
if (!states.has(id)) states.set(id, { change: 0, saved: 0, timer: null, promise: null, error: null, locked: false, getData: null });
|
|
14
|
+
return states.get(id);
|
|
15
|
+
}
|
|
16
|
+
function notify(id) { options.onStatus?.(id, status(id)); }
|
|
17
|
+
function status(id) {
|
|
18
|
+
const current = state(id);
|
|
19
|
+
if (current.locked) return { state: "error", text: current.error?.message || "외부 변경과 현재 편집이 충돌했습니다. 입력 내용은 보존되었습니다." };
|
|
20
|
+
if (current.error) return { state: "error", text: `저장 실패 · ${current.error.message}` };
|
|
21
|
+
if (current.promise) return { state: "saving", text: "자동 저장 중…" };
|
|
22
|
+
if (current.change > current.saved) return { state: "dirty", text: "변경사항을 자동 저장할 예정입니다." };
|
|
23
|
+
return { state: "saved", text: "자동 저장됨" };
|
|
24
|
+
}
|
|
25
|
+
function schedule(id, getData) {
|
|
26
|
+
const current = state(id);
|
|
27
|
+
if (globalError || current.locked) return false;
|
|
28
|
+
current.change += 1;
|
|
29
|
+
current.error = null;
|
|
30
|
+
current.getData = getData;
|
|
31
|
+
if (current.timer) clearTimeout(current.timer);
|
|
32
|
+
current.timer = setTimeout(() => { current.timer = null; void flush(id).catch(() => {}); }, delay);
|
|
33
|
+
notify(id);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
async function flush(id) {
|
|
37
|
+
const current = state(id);
|
|
38
|
+
if (globalError) throw globalError;
|
|
39
|
+
if (current.locked) throw current.error;
|
|
40
|
+
if (current.promise) return current.promise;
|
|
41
|
+
if (current.saved >= current.change) return;
|
|
42
|
+
if (current.timer) { clearTimeout(current.timer); current.timer = null; }
|
|
43
|
+
const targetChange = current.change;
|
|
44
|
+
const submitted = options.clone(current.getData());
|
|
45
|
+
options.validate(id, submitted);
|
|
46
|
+
current.promise = (async () => {
|
|
47
|
+
try {
|
|
48
|
+
await options.save(id, submitted);
|
|
49
|
+
if (globalError) throw globalError;
|
|
50
|
+
current.saved = targetChange;
|
|
51
|
+
current.error = null;
|
|
52
|
+
options.onSaved?.(id, submitted);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
current.error = error;
|
|
55
|
+
if (options.lockOnError === true || options.isConflict?.(error)) current.locked = true;
|
|
56
|
+
throw error;
|
|
57
|
+
} finally {
|
|
58
|
+
current.promise = null;
|
|
59
|
+
notify(id);
|
|
60
|
+
}
|
|
61
|
+
if (current.change > current.saved) return flush(id);
|
|
62
|
+
})();
|
|
63
|
+
notify(id);
|
|
64
|
+
return current.promise;
|
|
65
|
+
}
|
|
66
|
+
async function flushAll(ids = [...states.keys()]) {
|
|
67
|
+
ids.forEach(id => {
|
|
68
|
+
const current = state(id);
|
|
69
|
+
if (current.timer) { clearTimeout(current.timer); current.timer = null; }
|
|
70
|
+
});
|
|
71
|
+
const results = await Promise.allSettled(ids.map(flush));
|
|
72
|
+
const failed = results.find(result => result.status === "rejected");
|
|
73
|
+
if (failed) throw failed.reason;
|
|
74
|
+
}
|
|
75
|
+
function hasPendingOrFailure(ids = [...states.keys()]) {
|
|
76
|
+
return ids.some(id => {
|
|
77
|
+
const current = state(id);
|
|
78
|
+
return current.change > current.saved || current.promise || current.error || current.locked;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function recordError(id, error) {
|
|
82
|
+
if (options.isConflict?.(error)) return lockAll(error);
|
|
83
|
+
const current = state(id);
|
|
84
|
+
current.error = error;
|
|
85
|
+
if (options.lockOnError === true || options.isConflict?.(error)) current.locked = true;
|
|
86
|
+
notify(id);
|
|
87
|
+
}
|
|
88
|
+
function clearError(id) {
|
|
89
|
+
const current = state(id);
|
|
90
|
+
if (current.locked) return false;
|
|
91
|
+
current.error = null;
|
|
92
|
+
notify(id);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
function lockAll(error, ids = [...states.keys()]) {
|
|
96
|
+
if (!globalError) globalError = error instanceof Error ? error : new Error(String(error || "새로고침이 필요합니다."));
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
const current = state(id);
|
|
99
|
+
if (current.timer) clearTimeout(current.timer);
|
|
100
|
+
current.timer = null;
|
|
101
|
+
current.error = globalError;
|
|
102
|
+
current.locked = true;
|
|
103
|
+
notify(id);
|
|
104
|
+
}
|
|
105
|
+
return globalError;
|
|
106
|
+
}
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
state,
|
|
109
|
+
status,
|
|
110
|
+
schedule,
|
|
111
|
+
flush,
|
|
112
|
+
flushAll,
|
|
113
|
+
hasPendingOrFailure,
|
|
114
|
+
recordError,
|
|
115
|
+
clearError,
|
|
116
|
+
lockAll,
|
|
117
|
+
get globalError() { return globalError; }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return Object.freeze({ create });
|
|
121
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchComponentCatalogContract = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const IMPORT_PROFILE = "writer-import-v1";
|
|
9
|
+
const TEMPLATE_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
|
|
10
|
+
const INSTANCE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
11
|
+
const SUPPORTED_MODES = Object.freeze(["authoring", "preview"]);
|
|
12
|
+
const ACCESS_MODES = new Set(["read", "read_write"]);
|
|
13
|
+
const GUIDE_PREVIEW_STATES = new Set(["integrated", "context", "authoring", "internal"]);
|
|
14
|
+
|
|
15
|
+
function binding(key, dataTemplateId, accessMode, required = true) {
|
|
16
|
+
return { key, dataTemplateId, accessMode, required };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function view(templateId, instanceId, label, rendererId, bindings) {
|
|
20
|
+
return { templateId, instanceId, label, rendererId, supportedModes: SUPPORTED_MODES, bindings };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function guide(purpose, example, authoring, previewState, preview, relatedTemplateIds, imageAlt) {
|
|
24
|
+
return {
|
|
25
|
+
purpose, example, authoring,
|
|
26
|
+
preview: { state: previewState, description: preview },
|
|
27
|
+
relatedTemplateIds,
|
|
28
|
+
image: { src: "", alt: imageAlt }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const RAW_CATALOG = [
|
|
33
|
+
{
|
|
34
|
+
templateId: "ssobig.basic", defaultInstanceId: "basic", tabLabel: "기본정보", required: true,
|
|
35
|
+
description: "작품 제목과 소개, 플레이 인원, 화면 테마 등 기본 설정을 관리합니다.",
|
|
36
|
+
guide: guide("작품 전체의 정체성과 화면 분위기를 정합니다.", "작품명, 한 줄 소개, 4인용 설정과 대표 색상을 입력합니다.", "제목·인원·소개와 포스터·로고·배경·색상·폰트를 설정합니다.", "integrated", "작품 제목과 테마가 제작 화면과 캐릭터 관점 미리보기 전체에 적용됩니다.", ["ssobig.assets"], "기본정보 제작 화면 예시"),
|
|
37
|
+
defaultData: null,
|
|
38
|
+
view: view("ssobig.view.basic-workbench", "basic-workbench", "기본정보 작업 화면", "ssobig.basic", [
|
|
39
|
+
binding("primary", "ssobig.basic", "read_write"), binding("assets", "ssobig.assets", "read_write")
|
|
40
|
+
]),
|
|
41
|
+
importProfiles: [IMPORT_PROFILE]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
templateId: "ssobig.progress", defaultInstanceId: "progress", tabLabel: "진행", required: true,
|
|
45
|
+
description: "게임의 단계별 순서와 시간, 플레이어 안내와 진행 행동을 구성합니다.",
|
|
46
|
+
guide: guide("게임이 어떤 순서와 속도로 진행되는지 설계합니다.", "프롤로그, 1차 조사, 토론, 최종 선택 순서와 시간을 작성합니다.", "단계 이름·시간·안내문·진행 행동을 순서대로 편집합니다.", "integrated", "현재 캐릭터가 확인할 진행 단계와 안내가 통합 미리보기에 표시됩니다.", ["ssobig.common", "ssobig.ending"], "진행 Component 제작 화면 예시"),
|
|
47
|
+
defaultData: { steps: [] },
|
|
48
|
+
view: view("ssobig.view.progress-workbench", "progress-workbench", "진행 작업 화면", "ssobig.progress", [
|
|
49
|
+
binding("primary", "ssobig.progress", "read_write")
|
|
50
|
+
]),
|
|
51
|
+
importProfiles: [IMPORT_PROFILE]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
templateId: "ssobig.common", defaultInstanceId: "common", tabLabel: "공통정보", required: true,
|
|
55
|
+
description: "모든 플레이어에게 함께 보여줄 게임 규칙과 안내 문서를 작성합니다.",
|
|
56
|
+
guide: guide("모든 참가자가 공통으로 읽는 규칙과 배경 정보를 작성합니다.", "프롤로그, 조사 규칙, 주의사항과 공통 세계관을 구성합니다.", "기본 문서와 자유 문서의 제목·본문·노출 순서를 편집합니다.", "integrated", "공통 문서는 모든 캐릭터 관점의 통합 미리보기에 함께 표시됩니다.", ["ssobig.progress", "ssobig.character"], "공통정보 제작 화면 예시"),
|
|
57
|
+
defaultData: { sections: {}, customSections: [], sectionOrder: [], baseTitles: {}, deletedBaseSections: [] },
|
|
58
|
+
view: view("ssobig.view.common-workbench", "common-workbench", "공통정보 작업 화면", "ssobig.common", [
|
|
59
|
+
binding("primary", "ssobig.common", "read_write")
|
|
60
|
+
]),
|
|
61
|
+
importProfiles: [IMPORT_PROFILE]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
templateId: "ssobig.choice", defaultInstanceId: "choice", tabLabel: "선택",
|
|
65
|
+
description: "플레이어가 게임 중간이나 최종 단계에서 고를 수 있는 선택 정보를 작성합니다.",
|
|
66
|
+
guide: guide("플레이어에게 제시할 선택지와 선택에 필요한 설명을 정리합니다.", "중간 조사 방향이나 최종 범인 지목처럼 플레이어가 결정할 항목을 문서로 구성합니다.", "공통정보와 같은 문서 카드에서 제목·본문·노출 순서를 편집합니다.", "authoring", "선택 문서는 제작 화면의 미리보기에서 공통정보와 같은 카드 형식으로 확인합니다.", ["ssobig.progress", "ssobig.ending"], "선택 제작 화면 예시"),
|
|
67
|
+
defaultData: { sections: {}, customSections: [], sectionOrder: [], baseTitles: {}, deletedBaseSections: [] },
|
|
68
|
+
view: view("ssobig.view.choice-workbench", "choice-workbench", "선택 작업 화면", "ssobig.choice", [
|
|
69
|
+
binding("primary", "ssobig.choice", "read_write")
|
|
70
|
+
])
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
templateId: "ssobig.character", defaultInstanceId: "characters", tabLabel: "캐릭터", required: true,
|
|
74
|
+
description: "캐릭터의 이름과 이미지, 공개 정보와 개인 이야기를 작성합니다.",
|
|
75
|
+
guide: guide("플레이어 캐릭터와 NPC, 각 인물에게 전달할 정보를 설계합니다.", "탐정, 의뢰인 등 인물별 유형과 자유로운 제목·본문 원고를 작성합니다.", "기본정보 Container와 추가 Container에 캐릭터 원고 section을 배치합니다.", "integrated", "플레이어 캐릭터로 지정한 인물의 Container별 원고가 통합 미리보기에 표시됩니다.", ["ssobig.assets", "ssobig.timeline", "ssobig.investigation-board"], "캐릭터 제작 화면 예시"),
|
|
76
|
+
defaultData: { characters: {}, names: {}, order: [], customCharacters: [], deletedColumns: [] },
|
|
77
|
+
view: view("ssobig.view.character-workbench", "character-workbench", "캐릭터 작업 화면", "ssobig.character", [
|
|
78
|
+
binding("primary", "ssobig.character", "read_write"), binding("assets", "ssobig.assets", "read_write")
|
|
79
|
+
]),
|
|
80
|
+
importProfiles: [IMPORT_PROFILE]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
templateId: "ssobig.clues", defaultInstanceId: "clues", tabLabel: "단서",
|
|
84
|
+
description: "플레이어가 조사하고 획득할 단서의 이미지와 내용을 관리합니다.",
|
|
85
|
+
guide: guide("조사 과정에서 발견하는 증거와 정보를 정리합니다.", "발견 장소, 단서 이미지, 공개 설명과 작가 메모를 등록합니다.", "단서 이름·제목·설명과 선택 필드·이미지를 표 형태로 편집합니다.", "context", "단서 조합·AI 응답·추리 보드가 단서 ID와 현재 내용을 참고합니다.", ["ssobig.assets", "ssobig.clue-combinations", "ssobig.ai-response", "ssobig.investigation-board"], "단서 제작 화면 예시"),
|
|
86
|
+
defaultData: { clues: [], enabledOptionalFields: ["confirmed", "image", "location", "tags", "color", "secret"], writerNote: "" },
|
|
87
|
+
view: view("ssobig.view.clues-workbench", "clues-workbench", "단서 작업 화면", "ssobig.clues", [
|
|
88
|
+
binding("primary", "ssobig.clues", "read_write"), binding("assets", "ssobig.assets", "read_write")
|
|
89
|
+
]),
|
|
90
|
+
importProfiles: [IMPORT_PROFILE]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
templateId: "ssobig.clue-combinations", defaultInstanceId: "clue-combinations", tabLabel: "단서 조합",
|
|
94
|
+
description: "여러 단서를 조합했을 때 발견되는 결과와 추가 정보를 설정합니다.",
|
|
95
|
+
guide: guide("여러 단서가 함께 사용될 때 새로 드러나는 결과를 설계합니다.", "열쇠와 잠긴 상자를 조합하면 비밀 문서가 발견되도록 설정합니다.", "조건 단서와 결과 단서·문구, 힌트를 연결합니다.", "authoring", "현재 통합 미리보기에는 독립 플레이 화면이 없으며 제작 규칙으로 관리됩니다.", ["ssobig.clues"], "단서 조합 제작 화면 예시"),
|
|
96
|
+
defaultData: { combinations: [] },
|
|
97
|
+
view: view("ssobig.view.clue-combinations-workbench", "clue-combinations-workbench", "단서 조합 작업 화면", "ssobig.clue-combinations", [
|
|
98
|
+
binding("primary", "ssobig.clue-combinations", "read_write"), binding("clue-context", "ssobig.clues", "read")
|
|
99
|
+
])
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
templateId: "ssobig.ai-response", defaultInstanceId: "ai-responses", tabLabel: "AI 응답",
|
|
103
|
+
description: "플레이어의 질문이나 입력에 따라 보여줄 답변을 설정합니다.",
|
|
104
|
+
guide: guide("특정 질문이나 문구에 대응하는 자동 답변을 준비합니다.", "인물 이름을 질문하면 준비된 증언이 표시되도록 작성합니다.", "입력 조건과 답변을 응답 단위로 편집합니다.", "authoring", "현재 통합 미리보기에는 독립 플레이 화면이 없으며 응답 규칙을 제작 화면에서 검토합니다.", ["ssobig.clues"], "AI 응답 제작 화면 예시"),
|
|
105
|
+
defaultData: { responses: [] },
|
|
106
|
+
view: view("ssobig.view.ai-response-workbench", "ai-response-workbench", "원혼응답 작업 화면", "ssobig.ai-response", [
|
|
107
|
+
binding("primary", "ssobig.ai-response", "read_write"), binding("clue-context", "ssobig.clues", "read", false)
|
|
108
|
+
]),
|
|
109
|
+
importProfiles: [IMPORT_PROFILE]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
templateId: "ssobig.timeline", defaultInstanceId: "timeline", tabLabel: "타임라인",
|
|
113
|
+
description: "사건이 일어난 시간과 장소, 캐릭터별 기억과 행동을 시간순으로 정리합니다.",
|
|
114
|
+
guide: guide("사건의 객관적 흐름과 인물별 기억을 시간축으로 맞춥니다.", "22시 로비 사건과 각 캐릭터가 기억하는 행동을 나란히 기록합니다.", "시간·장소·진실과 캐릭터별 기억을 사건 단위로 편집합니다.", "context", "캐릭터 정보를 읽어 인물별 기억을 연결하지만 독립 플레이 화면은 제공하지 않습니다.", ["ssobig.character"], "타임라인 제작 화면 예시"),
|
|
115
|
+
defaultData: { events: [] },
|
|
116
|
+
view: view("ssobig.view.timeline-workbench", "timeline-workbench", "타임라인 작업 화면", "ssobig.timeline", [
|
|
117
|
+
binding("primary", "ssobig.timeline", "read_write"), binding("character-context", "ssobig.character", "read", false)
|
|
118
|
+
]),
|
|
119
|
+
importProfiles: [IMPORT_PROFILE]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
templateId: "ssobig.ending", defaultInstanceId: "ending", tabLabel: "엔딩",
|
|
123
|
+
description: "플레이어의 선택과 게임 결과에 따라 공개될 엔딩을 설정합니다.",
|
|
124
|
+
guide: guide("엔딩이 공개되는 조건과 결말을 연결합니다.", "범인 지목 결과에 따라 성공·실패 결말이 열리도록 구성합니다.", "조건 코드·판정 설명과 결과 문서를 한 쌍으로 편집합니다.", "context", "게임 후 문서가 엔딩을 참고하며 현재 통합 미리보기에는 독립 화면이 없습니다.", ["ssobig.postgame"], "엔딩 제작 화면 예시"),
|
|
125
|
+
defaultData: { conditions: [], outcomes: [] },
|
|
126
|
+
view: view("ssobig.view.ending-workbench", "ending-workbench", "엔딩 작업 화면", "ssobig.ending", [
|
|
127
|
+
binding("primary", "ssobig.ending", "read_write")
|
|
128
|
+
]),
|
|
129
|
+
importProfiles: [IMPORT_PROFILE]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
templateId: "ssobig.postgame", defaultInstanceId: "postgame", tabLabel: "게임 후 문서",
|
|
133
|
+
description: "게임이 끝난 뒤 공개할 사건의 전말, 에필로그와 추가 문서를 작성합니다.",
|
|
134
|
+
guide: guide("게임 종료 뒤 참가자에게 공개할 정답과 후일담을 작성합니다.", "사건의 진실, 범행 과정, 캐릭터별 에필로그를 문서로 구성합니다.", "진실·에필로그 문서의 제목·본문과 추가 항목을 편집합니다.", "authoring", "엔딩을 참고하지만 현재 통합 미리보기에는 독립 플레이 화면이 없습니다.", ["ssobig.ending"], "게임 후 문서 제작 화면 예시"),
|
|
135
|
+
defaultData: { truth: [], epilogue: [], intro: { truth: "", epilogue: "" } },
|
|
136
|
+
view: view("ssobig.view.postgame-workbench", "postgame-workbench", "게임 후 문서 작업 화면", "ssobig.postgame", [
|
|
137
|
+
binding("primary", "ssobig.postgame", "read_write"), binding("ending-context", "ssobig.ending", "read", false)
|
|
138
|
+
]),
|
|
139
|
+
importProfiles: [IMPORT_PROFILE]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
templateId: "ssobig.author-notes", defaultInstanceId: "author-notes", tabLabel: "작가노트",
|
|
143
|
+
description: "작품 제작과 게임 운영에 필요한 메모를 기록합니다.",
|
|
144
|
+
guide: guide("플레이어에게 공개하지 않을 제작·운영 메모를 보관합니다.", "복선 점검, 진행자 주의사항과 다음 수정 목록을 기록합니다.", "제목과 본문으로 된 자유 메모를 추가하고 정리합니다.", "authoring", "작가 전용 Component로 통합 미리보기에 표시되지 않습니다.", [], "작가노트 제작 화면 예시"),
|
|
145
|
+
defaultData: { notes: [] },
|
|
146
|
+
view: view("ssobig.view.author-notes-workbench", "author-notes-workbench", "작가노트 작업 화면", "ssobig.author-notes", [
|
|
147
|
+
binding("primary", "ssobig.author-notes", "read_write")
|
|
148
|
+
]),
|
|
149
|
+
importProfiles: [IMPORT_PROFILE]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
templateId: "ssobig.investigation-board", defaultInstanceId: "investigation-board", tabLabel: "추리 보드",
|
|
153
|
+
description: "캐릭터와 단서의 관계를 배치해 사건 구조를 한눈에 정리합니다.",
|
|
154
|
+
guide: guide("캐릭터·단서·메모의 관계를 공간적으로 검토합니다.", "중심 사건 주변에 인물과 증거를 배치하고 관계선을 연결합니다.", "참조 카드와 포스트잇을 이동·크기 조정하고 관계선을 편집합니다.", "context", "캐릭터·단서·에셋의 현재 값을 읽어 보드에 표시하며 독립 플레이 화면은 없습니다.", ["ssobig.character", "ssobig.clues", "ssobig.assets"], "추리 보드 제작 화면 예시"),
|
|
155
|
+
defaultData: { viewport: { width: 1600, height: 1000 }, nodes: [], edges: [] },
|
|
156
|
+
view: view("ssobig.view.investigation-board-workbench", "investigation-board-workbench", "추리 보드 작업 화면", "ssobig.investigation-board", [
|
|
157
|
+
binding("primary", "ssobig.investigation-board", "read_write"),
|
|
158
|
+
binding("character-context", "ssobig.character", "read"),
|
|
159
|
+
binding("clue-context", "ssobig.clues", "read", false),
|
|
160
|
+
binding("assets", "ssobig.assets", "read")
|
|
161
|
+
])
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
templateId: "ssobig.assets", defaultInstanceId: "assets", tabLabel: "에셋", required: true, internal: true,
|
|
165
|
+
description: "작품에서 사용하는 JPEG·PNG·WebP·GIF 이미지와 JSON 파일을 한곳에서 등록하고 관리합니다.",
|
|
166
|
+
guide: guide("다른 Component가 사용하는 파일의 안전한 포인터를 한곳에서 소유합니다.", "포스터, 로고, 캐릭터·단서 이미지와 보조 JSON을 등록합니다.", "작품 구성의 에셋 탭에서 파일 미리보기·교체·연결 해제를 관리합니다.", "internal", "일반 제작 탭에는 나타나지 않는 내부 필수 Component입니다.", ["ssobig.basic", "ssobig.character", "ssobig.clues", "ssobig.investigation-board"], "에셋 관리 화면 예시"),
|
|
167
|
+
defaultConfig: { hidden: true }, defaultData: { assets: {} }, importProfiles: [IMPORT_PROFILE]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
templateId: "ssobig.documents", defaultInstanceId: "document", tabLabel: "범용 문서", allowMultiple: true,
|
|
171
|
+
description: "작품에 필요한 별도의 문서를 자유롭게 추가하고 관리합니다.",
|
|
172
|
+
guide: guide("고정된 Component에 속하지 않는 보조 문서를 자유롭게 만듭니다.", "진행자 체크리스트나 별도 부록을 독립 탭으로 추가합니다.", "블록 단위의 자유 문서를 작성하며 작품마다 여러 탭을 만들 수 있습니다.", "authoring", "현재 통합 미리보기에는 자동 포함되지 않는 제작용 문서입니다.", [], "범용 문서 제작 화면 예시"),
|
|
173
|
+
defaultData: { blocks: [] },
|
|
174
|
+
view: view("ssobig.view.documents-workbench", "documents-workbench", "범용 문서 작업 화면", "ssobig.documents", [
|
|
175
|
+
binding("primary", "ssobig.documents", "read_write")
|
|
176
|
+
])
|
|
177
|
+
}
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
function isObject(value) { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }
|
|
181
|
+
function clone(value) {
|
|
182
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
183
|
+
if (isObject(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clone(item)]));
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
function deepFreeze(value) {
|
|
187
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
188
|
+
Object.values(value).forEach(deepFreeze);
|
|
189
|
+
return Object.freeze(value);
|
|
190
|
+
}
|
|
191
|
+
function assertKeys(value, allowed, label) {
|
|
192
|
+
if (!isObject(value)) throw new TypeError(`${label}은 객체여야 합니다.`);
|
|
193
|
+
const unexpected = Object.keys(value).find(key => !allowed.includes(key));
|
|
194
|
+
if (unexpected) throw new TypeError(`${label}.${unexpected}은 지원되지 않습니다.`);
|
|
195
|
+
}
|
|
196
|
+
function assertTemplateId(value, label) {
|
|
197
|
+
const id = String(value || "");
|
|
198
|
+
if (!TEMPLATE_PATTERN.test(id)) throw new TypeError(`${label} 식별자가 올바르지 않습니다: ${id || "(비어 있음)"}`);
|
|
199
|
+
return id;
|
|
200
|
+
}
|
|
201
|
+
function assertInstanceId(value, label) {
|
|
202
|
+
const id = String(value || "");
|
|
203
|
+
if (!INSTANCE_PATTERN.test(id)) throw new TypeError(`${label} 식별자가 올바르지 않습니다: ${id || "(비어 있음)"}`);
|
|
204
|
+
return id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeBinding(input, viewTemplateId) {
|
|
208
|
+
assertKeys(input, ["key", "dataTemplateId", "accessMode", "required"], `${viewTemplateId}.binding`);
|
|
209
|
+
const key = assertInstanceId(input.key, `${viewTemplateId}.binding.key`);
|
|
210
|
+
const dataTemplateId = assertTemplateId(input.dataTemplateId, `${viewTemplateId}.${key}.dataTemplateId`);
|
|
211
|
+
const accessMode = String(input.accessMode || "");
|
|
212
|
+
if (!ACCESS_MODES.has(accessMode)) throw new TypeError(`${viewTemplateId}.${key}.accessMode가 올바르지 않습니다.`);
|
|
213
|
+
return { key, dataTemplateId, accessMode, required: input.required !== false };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeView(input, ownerTemplateId) {
|
|
217
|
+
if (input == null) return null;
|
|
218
|
+
assertKeys(input, ["templateId", "instanceId", "label", "rendererId", "supportedModes", "bindings"], `${ownerTemplateId}.view`);
|
|
219
|
+
const templateId = assertTemplateId(input.templateId, `${ownerTemplateId}.view.templateId`);
|
|
220
|
+
const instanceId = assertInstanceId(input.instanceId, `${ownerTemplateId}.view.instanceId`);
|
|
221
|
+
const rendererId = assertTemplateId(input.rendererId, `${ownerTemplateId}.view.rendererId`);
|
|
222
|
+
if (rendererId !== ownerTemplateId) throw new TypeError(`${templateId} renderer는 소유 Data Template과 같아야 합니다.`);
|
|
223
|
+
const supportedModes = Array.isArray(input.supportedModes) ? [...input.supportedModes].map(String) : [];
|
|
224
|
+
if (supportedModes.length !== SUPPORTED_MODES.length
|
|
225
|
+
|| new Set(supportedModes).size !== supportedModes.length
|
|
226
|
+
|| supportedModes.some(mode => !SUPPORTED_MODES.includes(mode))) {
|
|
227
|
+
throw new TypeError(`${templateId}.supportedModes가 올바르지 않습니다.`);
|
|
228
|
+
}
|
|
229
|
+
const bindings = (Array.isArray(input.bindings) ? input.bindings : []).map(item => normalizeBinding(item, templateId));
|
|
230
|
+
if (new Set(bindings.map(item => item.key)).size !== bindings.length) throw new TypeError(`${templateId} binding key가 중복되었습니다.`);
|
|
231
|
+
const primary = bindings.find(item => item.key === "primary");
|
|
232
|
+
if (!primary || primary.dataTemplateId !== ownerTemplateId || primary.accessMode !== "read_write" || !primary.required) {
|
|
233
|
+
throw new TypeError(`${templateId}에는 소유 Data Template의 필수 read_write primary binding이 필요합니다.`);
|
|
234
|
+
}
|
|
235
|
+
return { templateId, instanceId, label: String(input.label || templateId), rendererId, supportedModes, bindings };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeDescriptor(input) {
|
|
239
|
+
assertKeys(input, [
|
|
240
|
+
"templateId", "defaultInstanceId", "tabLabel", "description", "required", "internal", "allowMultiple",
|
|
241
|
+
"defaultConfig", "defaultData", "view", "importProfiles", "guide"
|
|
242
|
+
], "Component descriptor");
|
|
243
|
+
const templateId = assertTemplateId(input.templateId, "Component templateId");
|
|
244
|
+
const defaultConfig = input.defaultConfig === undefined ? {} : input.defaultConfig;
|
|
245
|
+
if (!isObject(defaultConfig)) throw new TypeError(`${templateId}.defaultConfig는 객체여야 합니다.`);
|
|
246
|
+
if (input.defaultData !== null && !isObject(input.defaultData)) throw new TypeError(`${templateId}.defaultData는 객체 또는 null이어야 합니다.`);
|
|
247
|
+
const importProfiles = Array.isArray(input.importProfiles) ? [...input.importProfiles].map(String) : [];
|
|
248
|
+
if (importProfiles.some(profile => !INSTANCE_PATTERN.test(profile)) || new Set(importProfiles).size !== importProfiles.length) {
|
|
249
|
+
throw new TypeError(`${templateId}.importProfiles가 올바르지 않습니다.`);
|
|
250
|
+
}
|
|
251
|
+
const guideValue = normalizeGuide(input.guide, templateId);
|
|
252
|
+
return {
|
|
253
|
+
templateId,
|
|
254
|
+
defaultInstanceId: assertInstanceId(input.defaultInstanceId, `${templateId}.defaultInstanceId`),
|
|
255
|
+
tabLabel: String(input.tabLabel || "").trim(),
|
|
256
|
+
description: String(input.description || ""),
|
|
257
|
+
required: input.required === true,
|
|
258
|
+
internal: input.internal === true,
|
|
259
|
+
allowMultiple: input.allowMultiple === true,
|
|
260
|
+
defaultConfig: clone(defaultConfig),
|
|
261
|
+
defaultData: clone(input.defaultData),
|
|
262
|
+
view: normalizeView(input.view, templateId),
|
|
263
|
+
importProfiles,
|
|
264
|
+
guide: guideValue
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizeGuide(input, templateId) {
|
|
269
|
+
assertKeys(input, ["purpose", "example", "authoring", "preview", "relatedTemplateIds", "image"], `${templateId}.guide`);
|
|
270
|
+
assertKeys(input.preview, ["state", "description"], `${templateId}.guide.preview`);
|
|
271
|
+
assertKeys(input.image, ["src", "alt"], `${templateId}.guide.image`);
|
|
272
|
+
const state = String(input.preview.state || "");
|
|
273
|
+
if (!GUIDE_PREVIEW_STATES.has(state)) throw new TypeError(`${templateId}.guide.preview.state가 올바르지 않습니다.`);
|
|
274
|
+
const relatedTemplateIds = Array.isArray(input.relatedTemplateIds) ? input.relatedTemplateIds.map(String) : [];
|
|
275
|
+
if (new Set(relatedTemplateIds).size !== relatedTemplateIds.length || relatedTemplateIds.includes(templateId)) {
|
|
276
|
+
throw new TypeError(`${templateId}.guide.relatedTemplateIds가 올바르지 않습니다.`);
|
|
277
|
+
}
|
|
278
|
+
const requiredText = ["purpose", "example", "authoring"].reduce((output, key) => {
|
|
279
|
+
const value = String(input[key] || "").trim();
|
|
280
|
+
if (!value) throw new TypeError(`${templateId}.guide.${key}가 비어 있습니다.`);
|
|
281
|
+
output[key] = value;
|
|
282
|
+
return output;
|
|
283
|
+
}, {});
|
|
284
|
+
const previewDescription = String(input.preview.description || "").trim();
|
|
285
|
+
const imageAlt = String(input.image.alt || "").trim();
|
|
286
|
+
if (!previewDescription || !imageAlt) throw new TypeError(`${templateId}.guide 설명과 이미지 대체 텍스트가 필요합니다.`);
|
|
287
|
+
return {
|
|
288
|
+
...requiredText,
|
|
289
|
+
preview: { state, description: previewDescription },
|
|
290
|
+
relatedTemplateIds,
|
|
291
|
+
image: { src: `./assets/component-guide/${templateId.replace(/\./g, "-")}.webp`, alt: imageAlt }
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createCatalog(input) {
|
|
296
|
+
if (!Array.isArray(input) || !input.length) throw new TypeError("Component catalog가 비어 있습니다.");
|
|
297
|
+
const descriptors = input.map(normalizeDescriptor);
|
|
298
|
+
const byId = new Map();
|
|
299
|
+
const viewIds = new Set();
|
|
300
|
+
const instanceIds = new Set();
|
|
301
|
+
const viewInstanceIds = new Set();
|
|
302
|
+
for (const descriptor of descriptors) {
|
|
303
|
+
if (byId.has(descriptor.templateId)) throw new TypeError(`Component Template이 중복되었습니다: ${descriptor.templateId}`);
|
|
304
|
+
if (instanceIds.has(descriptor.defaultInstanceId)) throw new TypeError(`기본 Component Instance가 중복되었습니다: ${descriptor.defaultInstanceId}`);
|
|
305
|
+
byId.set(descriptor.templateId, descriptor);
|
|
306
|
+
instanceIds.add(descriptor.defaultInstanceId);
|
|
307
|
+
if (descriptor.view && viewIds.has(descriptor.view.templateId)) throw new TypeError(`View Template이 중복되었습니다: ${descriptor.view.templateId}`);
|
|
308
|
+
if (descriptor.view && viewInstanceIds.has(descriptor.view.instanceId)) throw new TypeError(`기본 View Instance가 중복되었습니다: ${descriptor.view.instanceId}`);
|
|
309
|
+
if (descriptor.view) {
|
|
310
|
+
viewIds.add(descriptor.view.templateId);
|
|
311
|
+
viewInstanceIds.add(descriptor.view.instanceId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const descriptor of descriptors) {
|
|
315
|
+
if (descriptor.internal && descriptor.view) throw new TypeError(`${descriptor.templateId} 내부 Component는 소유 View를 가질 수 없습니다.`);
|
|
316
|
+
for (const binding of descriptor.view?.bindings || []) {
|
|
317
|
+
if (!byId.has(binding.dataTemplateId)) throw new TypeError(`${descriptor.view.templateId}.${binding.key}가 미등록 Data Template을 가리킵니다.`);
|
|
318
|
+
}
|
|
319
|
+
for (const relatedTemplateId of descriptor.guide.relatedTemplateIds) {
|
|
320
|
+
if (!byId.has(relatedTemplateId)) throw new TypeError(`${descriptor.templateId}.guide가 미등록 Template을 가리킵니다: ${relatedTemplateId}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
deepFreeze(descriptors);
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
list: () => descriptors.slice(),
|
|
326
|
+
get: templateId => byId.get(String(templateId || "")) || null,
|
|
327
|
+
forImportProfile: profile => descriptors.filter(item => item.importProfiles.includes(String(profile || "")))
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const catalog = createCatalog(RAW_CATALOG);
|
|
332
|
+
return Object.freeze({
|
|
333
|
+
IMPORT_PROFILE,
|
|
334
|
+
COMPONENTS: deepFreeze(catalog.list()),
|
|
335
|
+
createCatalog,
|
|
336
|
+
get: catalog.get,
|
|
337
|
+
list: catalog.list,
|
|
338
|
+
forImportProfile: catalog.forImportProfile
|
|
339
|
+
});
|
|
340
|
+
});
|