@things-factory/board-ui 10.0.0-beta.7 → 10.0.0-beta.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/apptools/favorite-tool.js +5 -5
- package/dist-client/apptools/favorite-tool.js.map +1 -1
- package/dist-client/board-list/board-tile-list.d.ts +6 -1
- package/dist-client/board-list/board-tile-list.js +291 -44
- package/dist-client/board-list/board-tile-list.js.map +1 -1
- package/dist-client/board-list/group-bar.js +3 -3
- package/dist-client/board-list/group-bar.js.map +1 -1
- package/dist-client/board-list/play-group-bar.d.ts +0 -1
- package/dist-client/board-list/play-group-bar.js +3 -6
- package/dist-client/board-list/play-group-bar.js.map +1 -1
- package/dist-client/board-provider.js +20 -8
- package/dist-client/board-provider.js.map +1 -1
- package/dist-client/data-grist/board-editor.js +4 -4
- package/dist-client/data-grist/board-editor.js.map +1 -1
- package/dist-client/data-grist/board-renderer.js +4 -4
- package/dist-client/data-grist/board-renderer.js.map +1 -1
- package/dist-client/graphql/board-template.js +1 -1
- package/dist-client/graphql/board-template.js.map +1 -1
- package/dist-client/graphql/board.d.ts +1 -0
- package/dist-client/graphql/board.js +28 -2
- package/dist-client/graphql/board.js.map +1 -1
- package/dist-client/graphql/group.js +1 -1
- package/dist-client/graphql/group.js.map +1 -1
- package/dist-client/graphql/play-group.js +3 -3
- package/dist-client/graphql/play-group.js.map +1 -1
- package/dist-client/pages/attachment-list-page.d.ts +16 -0
- package/dist-client/pages/attachment-list-page.js +63 -2
- package/dist-client/pages/attachment-list-page.js.map +1 -1
- package/dist-client/pages/board-action-dispatch.d.ts +31 -0
- package/dist-client/pages/board-action-dispatch.js +80 -0
- package/dist-client/pages/board-action-dispatch.js.map +1 -0
- package/dist-client/pages/board-action-dispatch.test.d.ts +1 -0
- package/dist-client/pages/board-action-dispatch.test.js +235 -0
- package/dist-client/pages/board-action-dispatch.test.js.map +1 -0
- package/dist-client/pages/board-edit-dispatch.d.ts +74 -0
- package/dist-client/pages/board-edit-dispatch.js +299 -0
- package/dist-client/pages/board-edit-dispatch.js.map +1 -0
- package/dist-client/pages/board-edit-dispatch.test.d.ts +1 -0
- package/dist-client/pages/board-edit-dispatch.test.js +858 -0
- package/dist-client/pages/board-edit-dispatch.test.js.map +1 -0
- package/dist-client/pages/board-list-page.d.ts +23 -3
- package/dist-client/pages/board-list-page.js +165 -77
- package/dist-client/pages/board-list-page.js.map +1 -1
- package/dist-client/pages/board-modeller-page.d.ts +134 -0
- package/dist-client/pages/board-modeller-page.js +725 -54
- package/dist-client/pages/board-modeller-page.js.map +1 -1
- package/dist-client/pages/board-player-by-name-page.js.map +1 -1
- package/dist-client/pages/board-player-page.js +14 -26
- package/dist-client/pages/board-player-page.js.map +1 -1
- package/dist-client/pages/board-viewer-by-name-page.d.ts +8 -1
- package/dist-client/pages/board-viewer-by-name-page.js +9 -1
- package/dist-client/pages/board-viewer-by-name-page.js.map +1 -1
- package/dist-client/pages/board-viewer-page.d.ts +2 -1
- package/dist-client/pages/board-viewer-page.js +52 -48
- package/dist-client/pages/board-viewer-page.js.map +1 -1
- package/dist-client/pages/play-list-page.d.ts +0 -1
- package/dist-client/pages/play-list-page.js +26 -33
- package/dist-client/pages/play-list-page.js.map +1 -1
- package/dist-client/pages/printable-board-viewer-page.js +2 -2
- package/dist-client/pages/printable-board-viewer-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js.map +1 -1
- package/dist-client/setting-let/board-view-setting-let.js +1 -1
- package/dist-client/setting-let/board-view-setting-let.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/notify-helper.d.ts +7 -0
- package/dist-client/utils/notify-helper.js +28 -0
- package/dist-client/utils/notify-helper.js.map +1 -0
- package/dist-client/utils/query-utils.d.ts +1 -0
- package/dist-client/utils/query-utils.js +20 -0
- package/dist-client/utils/query-utils.js.map +1 -0
- package/dist-client/viewparts/board-basic-info.js +9 -13
- package/dist-client/viewparts/board-basic-info.js.map +1 -1
- package/dist-client/viewparts/board-template-info.d.ts +0 -1
- package/dist-client/viewparts/board-template-info.js +5 -13
- package/dist-client/viewparts/board-template-info.js.map +1 -1
- package/dist-client/viewparts/board-versions.js +1 -1
- package/dist-client/viewparts/board-versions.js.map +1 -1
- package/dist-client/viewparts/group-info-basic.js +2 -2
- package/dist-client/viewparts/group-info-basic.js.map +1 -1
- package/dist-client/viewparts/group-info-import.js +2 -2
- package/dist-client/viewparts/group-info-import.js.map +1 -1
- package/dist-client/viewparts/link-builder.js +1 -1
- package/dist-client/viewparts/link-builder.js.map +1 -1
- package/dist-client/viewparts/play-group-info-basic.js +2 -2
- package/dist-client/viewparts/play-group-info-basic.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/translations/en.json +3 -29
- package/translations/ja.json +3 -29
- package/translations/ko.json +3 -29
- package/translations/ms.json +3 -29
- package/translations/zh.json +3 -29
|
@@ -5,8 +5,10 @@ import { customElement, query } from 'lit/decorators.js';
|
|
|
5
5
|
import gql from 'graphql-tag';
|
|
6
6
|
import { i18next, localize } from '@operato/i18n';
|
|
7
7
|
import { PageView } from '@operato/shell';
|
|
8
|
+
import { CommonButtonStyles } from '@operato/styles';
|
|
8
9
|
import { OxAttachmentList } from '@operato/attachment/ox-attachment-list.js';
|
|
9
10
|
import { client } from '@operato/graphql';
|
|
11
|
+
import { notify } from '@operato/layout';
|
|
10
12
|
async function fetchDataFromURL(fileURL) {
|
|
11
13
|
try {
|
|
12
14
|
const response = await fetch(fileURL);
|
|
@@ -57,7 +59,14 @@ let AttachmentListPage = class AttachmentListPage extends localize(i18next)(Page
|
|
|
57
59
|
},
|
|
58
60
|
importable: {
|
|
59
61
|
handler: this.importHandler.bind(this)
|
|
60
|
-
}
|
|
62
|
+
},
|
|
63
|
+
actions: [
|
|
64
|
+
{
|
|
65
|
+
title: i18next.t('button.backfill thumbnails'),
|
|
66
|
+
action: this.backfillThumbnails.bind(this),
|
|
67
|
+
...CommonButtonStyles.save
|
|
68
|
+
}
|
|
69
|
+
]
|
|
61
70
|
};
|
|
62
71
|
}
|
|
63
72
|
render() {
|
|
@@ -91,11 +100,63 @@ let AttachmentListPage = class AttachmentListPage extends localize(i18next)(Page
|
|
|
91
100
|
});
|
|
92
101
|
}
|
|
93
102
|
catch (err) {
|
|
94
|
-
console.
|
|
103
|
+
console.warn('Failed to process attachment:', name, err);
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
return data;
|
|
98
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* 썸네일이 없는 기존 첨부파일들에 대해 서버에서 일괄 썸네일 생성.
|
|
110
|
+
* 한 배치당 20 개씩, remaining 이 0 이 될 때까지 반복 호출.
|
|
111
|
+
*/
|
|
112
|
+
async backfillThumbnails() {
|
|
113
|
+
const BATCH = 20;
|
|
114
|
+
let total = { attempted: 0, succeeded: 0, failed: 0 };
|
|
115
|
+
let remaining = Number.POSITIVE_INFINITY;
|
|
116
|
+
notify({ message: i18next.t('text.backfilling thumbnails') });
|
|
117
|
+
while (remaining > 0) {
|
|
118
|
+
try {
|
|
119
|
+
const { data } = await client.mutate({
|
|
120
|
+
mutation: gql `
|
|
121
|
+
mutation BackfillAttachmentThumbnails($limit: Int) {
|
|
122
|
+
backfillAttachmentThumbnails(limit: $limit) {
|
|
123
|
+
attempted
|
|
124
|
+
succeeded
|
|
125
|
+
failed
|
|
126
|
+
remaining
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
`,
|
|
130
|
+
variables: { limit: BATCH }
|
|
131
|
+
});
|
|
132
|
+
const r = data?.backfillAttachmentThumbnails;
|
|
133
|
+
if (!r)
|
|
134
|
+
break;
|
|
135
|
+
total.attempted += r.attempted;
|
|
136
|
+
total.succeeded += r.succeeded;
|
|
137
|
+
total.failed += r.failed;
|
|
138
|
+
remaining = r.remaining;
|
|
139
|
+
if (r.attempted === 0)
|
|
140
|
+
break; // 더 이상 처리할 게 없음
|
|
141
|
+
// 한 배치에서 아무것도 성공하지 못했다면 무한 루프 방지를 위해 중단.
|
|
142
|
+
// 동일 후보들이 계속 실패하는 상황이므로 서버 측 원인 해결이 선행되어야 함.
|
|
143
|
+
if (r.succeeded === 0)
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error('backfill error:', err);
|
|
148
|
+
notify({ level: 'error', message: String(err) });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
notify({
|
|
153
|
+
message: i18next.t('text.thumbnail backfill done', {
|
|
154
|
+
succeeded: total.succeeded,
|
|
155
|
+
failed: total.failed
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
this.grist.fetch();
|
|
159
|
+
}
|
|
99
160
|
async importHandler(data, file) {
|
|
100
161
|
await client.mutate({
|
|
101
162
|
mutation: gql `
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-list-page.js","sourceRoot":"","sources":["../../client/pages/attachment-list-page.ts"],"names":[],"mappings":";AAAA,OAAO,2CAA2C,CAAA;AAElD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,GAAG,MAAM,aAAa,CAAA;AAE7B,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAA;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"attachment-list-page.js","sourceRoot":"","sources":["../../client/pages/attachment-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/attachment/ox-attachment-list.js'\n\nimport { css, html } from 'lit'\nimport { customElement, query } from 'lit/decorators.js'\nimport gql from 'graphql-tag'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { PageView } from '@operato/shell'\nimport { CommonButtonStyles } from '@operato/styles'\nimport { OxAttachmentList } from '@operato/attachment/ox-attachment-list.js'\nimport { client } from '@operato/graphql'\nimport { notify } from '@operato/layout'\n\nasync function fetchDataFromURL(fileURL: string) {\n try {\n const response = await fetch(fileURL)\n\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n\n const blob = await response.blob() // 파일 데이터를 Blob으로 변환\n\n if (blob.size === 0) {\n throw new Error('Content is empty') // Content가 비어 있으면 예외\n }\n\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n\n reader.onload = (event: any) => {\n const dataURL = event.target.result\n resolve(dataURL)\n }\n\n reader.onerror = (event: any) => {\n reject(event.error)\n }\n\n reader.readAsDataURL(blob) // Blob을 Data URL로 읽기\n })\n } catch (error) {\n throw error\n }\n}\n\n@customElement('attachment-list-page')\nexport class AttachmentListPage extends localize(i18next)(PageView) {\n static styles = [\n css`\n :host {\n display: flex;\n }\n `\n ]\n\n @query('ox-attachment-list') attachmentList!: OxAttachmentList\n\n get context() {\n return {\n title: i18next.t('title.attachment list'),\n search: {\n handler: search => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n board_topmenu: true,\n exportable: {\n name: i18next.t('title.attachment list'),\n data: this.exportHandler.bind(this)\n },\n importable: {\n handler: this.importHandler.bind(this)\n },\n actions: [\n {\n title: i18next.t('button.backfill thumbnails'),\n action: this.backfillThumbnails.bind(this),\n ...CommonButtonStyles.save\n }\n ]\n }\n }\n\n render() {\n return html` <ox-attachment-list .creatable=${true} without-search></ox-attachment-list> `\n }\n\n get grist() {\n return this.attachmentList.grist\n }\n\n languageUpdated(i18next) {\n this.attachmentList.requestUpdate()\n }\n\n async pageInitialized() {\n await this.updateComplete\n\n this.attachmentList.refreshAttachments()\n }\n\n async exportHandler() {\n const records = this.grist.data.records\n const data = {} as any\n\n for (const record of records) {\n const { id, name, mimetype, encoding, category, fullpath } = record\n\n try {\n const dataURL = await fetchDataFromURL(fullpath)\n\n id &&\n (data[id!] = {\n id,\n name,\n mimetype,\n encoding,\n category,\n contents: dataURL\n })\n } catch (err) {\n console.warn('Failed to process attachment:', name, err)\n }\n }\n\n return data\n }\n\n /**\n * 썸네일이 없는 기존 첨부파일들에 대해 서버에서 일괄 썸네일 생성.\n * 한 배치당 20 개씩, remaining 이 0 이 될 때까지 반복 호출.\n */\n async backfillThumbnails() {\n const BATCH = 20\n let total = { attempted: 0, succeeded: 0, failed: 0 }\n let remaining = Number.POSITIVE_INFINITY\n\n notify({ message: i18next.t('text.backfilling thumbnails') })\n\n while (remaining > 0) {\n try {\n const { data } = await client.mutate({\n mutation: gql`\n mutation BackfillAttachmentThumbnails($limit: Int) {\n backfillAttachmentThumbnails(limit: $limit) {\n attempted\n succeeded\n failed\n remaining\n }\n }\n `,\n variables: { limit: BATCH }\n })\n const r = data?.backfillAttachmentThumbnails\n if (!r) break\n total.attempted += r.attempted\n total.succeeded += r.succeeded\n total.failed += r.failed\n remaining = r.remaining\n\n if (r.attempted === 0) break // 더 이상 처리할 게 없음\n // 한 배치에서 아무것도 성공하지 못했다면 무한 루프 방지를 위해 중단.\n // 동일 후보들이 계속 실패하는 상황이므로 서버 측 원인 해결이 선행되어야 함.\n if (r.succeeded === 0) break\n } catch (err) {\n console.error('backfill error:', err)\n notify({ level: 'error', message: String(err) })\n return\n }\n }\n\n notify({\n message: i18next.t('text.thumbnail backfill done', {\n succeeded: total.succeeded,\n failed: total.failed\n })\n })\n this.grist.fetch()\n }\n\n async importHandler(data, file) {\n await client.mutate({\n mutation: gql`\n mutation importAttachments($file: Upload!) {\n importAttachments(file: $file) {\n id\n name\n description\n path\n }\n }\n `,\n variables: {\n file\n },\n context: { hasUpload: true }\n })\n\n this.grist.fetch()\n }\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host 측 BoardActionOp dispatcher — AI 가 지시한 ephemeral scene action 을
|
|
3
|
+
* things-scene API 로 실행.
|
|
4
|
+
*
|
|
5
|
+
* 별개 채널 — board-edit-patch 와 달리 model 변경 X, undo 영향 X, dirty flag 무관.
|
|
6
|
+
* pure function 형태 (스크롤되는 customElement 와 분리) — Lit 외부에서 단위 테스트 가능.
|
|
7
|
+
*/
|
|
8
|
+
import type { BoardActionOp } from '@things-factory/board-ai';
|
|
9
|
+
/**
|
|
10
|
+
* AI 의 BoardEditOp / BoardActionOp 가 지정한 대상 (id 또는 refid) 으로
|
|
11
|
+
* things-scene component 검색.
|
|
12
|
+
*
|
|
13
|
+
* id (사용자/AI 가 명시적으로 설정한 string identifier) 와
|
|
14
|
+
* refid (things-scene 이 모든 컴포넌트에 자동 발급하는 numeric identifier) 는
|
|
15
|
+
* 서로 별개 개념. 이 함수는 호스트 측에서 둘 중 어느 쪽으로든 lookup 가능하게 통합.
|
|
16
|
+
* refid 가 universal 이므로 우선.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findSceneComponent(scene: any, target: {
|
|
19
|
+
id?: string;
|
|
20
|
+
refid?: number;
|
|
21
|
+
}): any;
|
|
22
|
+
/**
|
|
23
|
+
* 단일 BoardActionOp 를 things-scene 에 적용.
|
|
24
|
+
*
|
|
25
|
+
* 호스트는 actions 배열을 순회하며 매 op 마다 호출. 실패 (잘못된 action /
|
|
26
|
+
* 누락된 컴포넌트 / API 예외) 시 `false` 반환 — 호스트가 warn 처리.
|
|
27
|
+
*
|
|
28
|
+
* setSceneMode 의 경우 호스트 측 reactive `mode` property 동기는 별도 (return
|
|
29
|
+
* 후 호스트가 scene.mode 재읽기). things-scene 의 mode: edit=1, view=0.
|
|
30
|
+
*/
|
|
31
|
+
export declare function dispatchBoardAction(scene: any, action: BoardActionOp): boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 의 BoardEditOp / BoardActionOp 가 지정한 대상 (id 또는 refid) 으로
|
|
3
|
+
* things-scene component 검색.
|
|
4
|
+
*
|
|
5
|
+
* id (사용자/AI 가 명시적으로 설정한 string identifier) 와
|
|
6
|
+
* refid (things-scene 이 모든 컴포넌트에 자동 발급하는 numeric identifier) 는
|
|
7
|
+
* 서로 별개 개념. 이 함수는 호스트 측에서 둘 중 어느 쪽으로든 lookup 가능하게 통합.
|
|
8
|
+
* refid 가 universal 이므로 우선.
|
|
9
|
+
*/
|
|
10
|
+
export function findSceneComponent(scene, target) {
|
|
11
|
+
if (!scene)
|
|
12
|
+
return null;
|
|
13
|
+
if (typeof target.refid === 'number') {
|
|
14
|
+
const byRefid = scene.rootContainer?.refidIndexMap?.get(target.refid);
|
|
15
|
+
if (byRefid)
|
|
16
|
+
return byRefid;
|
|
17
|
+
}
|
|
18
|
+
if (typeof target.id === 'string' && target.id.length > 0) {
|
|
19
|
+
return scene.findById?.(target.id) ?? null;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 단일 BoardActionOp 를 things-scene 에 적용.
|
|
25
|
+
*
|
|
26
|
+
* 호스트는 actions 배열을 순회하며 매 op 마다 호출. 실패 (잘못된 action /
|
|
27
|
+
* 누락된 컴포넌트 / API 예외) 시 `false` 반환 — 호스트가 warn 처리.
|
|
28
|
+
*
|
|
29
|
+
* setSceneMode 의 경우 호스트 측 reactive `mode` property 동기는 별도 (return
|
|
30
|
+
* 후 호스트가 scene.mode 재읽기). things-scene 의 mode: edit=1, view=0.
|
|
31
|
+
*/
|
|
32
|
+
export function dispatchBoardAction(scene, action) {
|
|
33
|
+
if (!scene || !action)
|
|
34
|
+
return false;
|
|
35
|
+
switch (action.action) {
|
|
36
|
+
case 'selectComponents': {
|
|
37
|
+
const refids = Array.isArray(action.refids) ? action.refids : [];
|
|
38
|
+
const targets = refids
|
|
39
|
+
.map(r => findSceneComponent(scene, { refid: r }))
|
|
40
|
+
.filter((c) => c);
|
|
41
|
+
scene.selected = targets;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
case 'centerToComponent': {
|
|
45
|
+
const target = findSceneComponent(scene, { refid: action.refid });
|
|
46
|
+
if (!target)
|
|
47
|
+
return false;
|
|
48
|
+
const animated = action.animated !== false;
|
|
49
|
+
scene.centerTo(target, animated);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
case 'fitToView': {
|
|
53
|
+
const mode = action.mode ?? 'fit';
|
|
54
|
+
scene.fit(mode);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
case 'setSceneMode': {
|
|
58
|
+
scene.mode = action.mode === 'edit' ? 1 : 0;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
case 'highlightComponents': {
|
|
62
|
+
// things-scene 의 highlightSearchResults API 위임 — 2D outline + 3D outline
|
|
63
|
+
// 모두 자동 처리 (operato-board 의 3D first-class UX).
|
|
64
|
+
const refids = Array.isArray(action.refids) ? action.refids : [];
|
|
65
|
+
const targets = refids
|
|
66
|
+
.map(r => findSceneComponent(scene, { refid: r }))
|
|
67
|
+
.filter((c) => c);
|
|
68
|
+
if (typeof scene.highlightSearchResults === 'function') {
|
|
69
|
+
scene.highlightSearchResults(targets);
|
|
70
|
+
}
|
|
71
|
+
// 변경된 시각 반영
|
|
72
|
+
if (typeof scene.invalidate === 'function')
|
|
73
|
+
scene.invalidate();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=board-action-dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"board-action-dispatch.js","sourceRoot":"","sources":["../../client/pages/board-action-dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Host 측 BoardActionOp dispatcher — AI 가 지시한 ephemeral scene action 을\n * things-scene API 로 실행.\n *\n * 별개 채널 — board-edit-patch 와 달리 model 변경 X, undo 영향 X, dirty flag 무관.\n * pure function 형태 (스크롤되는 customElement 와 분리) — Lit 외부에서 단위 테스트 가능.\n */\nimport type { BoardActionOp } from '@things-factory/board-ai'\n\n/**\n * AI 의 BoardEditOp / BoardActionOp 가 지정한 대상 (id 또는 refid) 으로\n * things-scene component 검색.\n *\n * id (사용자/AI 가 명시적으로 설정한 string identifier) 와\n * refid (things-scene 이 모든 컴포넌트에 자동 발급하는 numeric identifier) 는\n * 서로 별개 개념. 이 함수는 호스트 측에서 둘 중 어느 쪽으로든 lookup 가능하게 통합.\n * refid 가 universal 이므로 우선.\n */\nexport function findSceneComponent(\n scene: any,\n target: { id?: string; refid?: number }\n): any {\n if (!scene) return null\n if (typeof target.refid === 'number') {\n const byRefid = scene.rootContainer?.refidIndexMap?.get(target.refid)\n if (byRefid) return byRefid\n }\n if (typeof target.id === 'string' && target.id.length > 0) {\n return scene.findById?.(target.id) ?? null\n }\n return null\n}\n\n/**\n * 단일 BoardActionOp 를 things-scene 에 적용.\n *\n * 호스트는 actions 배열을 순회하며 매 op 마다 호출. 실패 (잘못된 action /\n * 누락된 컴포넌트 / API 예외) 시 `false` 반환 — 호스트가 warn 처리.\n *\n * setSceneMode 의 경우 호스트 측 reactive `mode` property 동기는 별도 (return\n * 후 호스트가 scene.mode 재읽기). things-scene 의 mode: edit=1, view=0.\n */\nexport function dispatchBoardAction(scene: any, action: BoardActionOp): boolean {\n if (!scene || !action) return false\n switch (action.action) {\n case 'selectComponents': {\n const refids = Array.isArray(action.refids) ? action.refids : []\n const targets = refids\n .map(r => findSceneComponent(scene, { refid: r }))\n .filter((c: any) => c)\n scene.selected = targets\n return true\n }\n case 'centerToComponent': {\n const target = findSceneComponent(scene, { refid: action.refid })\n if (!target) return false\n const animated = action.animated !== false\n scene.centerTo(target, animated)\n return true\n }\n case 'fitToView': {\n const mode = action.mode ?? 'fit'\n scene.fit(mode)\n return true\n }\n case 'setSceneMode': {\n scene.mode = action.mode === 'edit' ? 1 : 0\n return true\n }\n case 'highlightComponents': {\n // things-scene 의 highlightSearchResults API 위임 — 2D outline + 3D outline\n // 모두 자동 처리 (operato-board 의 3D first-class UX).\n const refids = Array.isArray(action.refids) ? action.refids : []\n const targets = refids\n .map(r => findSceneComponent(scene, { refid: r }))\n .filter((c: any) => c)\n if (typeof scene.highlightSearchResults === 'function') {\n scene.highlightSearchResults(targets)\n }\n // 변경된 시각 반영\n if (typeof scene.invalidate === 'function') scene.invalidate()\n return true\n }\n default:\n return false\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-action-dispatch.ts 의 pure helper 회귀 방지.
|
|
3
|
+
*
|
|
4
|
+
* 핵심 보호:
|
|
5
|
+
* - 가짜 scene 객체에 BoardActionOp 던져 실제 things-scene API 호출 인자 검증
|
|
6
|
+
* - findSceneComponent 의 refid 우선 / id fallback 동작
|
|
7
|
+
* - 잘못된 action / 누락 컴포넌트 시 false 반환 (silent fail 차단)
|
|
8
|
+
*
|
|
9
|
+
* AI 가 액션을 만들고 호스트가 things-scene 으로 정확히 매핑하지 않으면 사용자에게는
|
|
10
|
+
* "AI 응답은 왔는데 화면 그대로" 회귀로 보임 — 이 layer 가 그 사각지대.
|
|
11
|
+
*/
|
|
12
|
+
import { dispatchBoardAction, findSceneComponent } from './board-action-dispatch';
|
|
13
|
+
// ── Mock scene factory ──────────────────────────────────────────────
|
|
14
|
+
function makeMockScene(components = []) {
|
|
15
|
+
const refidIndexMap = new Map();
|
|
16
|
+
for (const c of components)
|
|
17
|
+
refidIndexMap.set(c.refid, { ...c, model: { ...c } });
|
|
18
|
+
const calls = [];
|
|
19
|
+
return {
|
|
20
|
+
scene: {
|
|
21
|
+
mode: 1, // edit
|
|
22
|
+
selected: [],
|
|
23
|
+
rootContainer: { refidIndexMap },
|
|
24
|
+
findById: (id) => {
|
|
25
|
+
for (const c of refidIndexMap.values()) {
|
|
26
|
+
if (c.id === id)
|
|
27
|
+
return c;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
},
|
|
31
|
+
centerTo(target, animated) {
|
|
32
|
+
calls.push({ method: 'centerTo', target, animated });
|
|
33
|
+
},
|
|
34
|
+
fit(mode) {
|
|
35
|
+
calls.push({ method: 'fit', mode });
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
calls,
|
|
39
|
+
refidIndexMap
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
describe('findSceneComponent', () => {
|
|
43
|
+
test('refid 매칭 — refidIndexMap 에서 lookup', () => {
|
|
44
|
+
const { scene } = makeMockScene([{ refid: 35, id: 'motor-1' }]);
|
|
45
|
+
const r = findSceneComponent(scene, { refid: 35 });
|
|
46
|
+
expect(r?.refid).toBe(35);
|
|
47
|
+
expect(r?.id).toBe('motor-1');
|
|
48
|
+
});
|
|
49
|
+
test('refid 가 없는데 매칭 시도 → null', () => {
|
|
50
|
+
const { scene } = makeMockScene([{ refid: 35 }]);
|
|
51
|
+
expect(findSceneComponent(scene, { refid: 999 })).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
test('id 매칭 — refid 미지정, findById fallback', () => {
|
|
54
|
+
const { scene } = makeMockScene([{ refid: 1, id: 'motor-1' }]);
|
|
55
|
+
const r = findSceneComponent(scene, { id: 'motor-1' });
|
|
56
|
+
expect(r?.id).toBe('motor-1');
|
|
57
|
+
});
|
|
58
|
+
test('refid 우선 — refid + id 둘 다 주어져도 refid 가 매칭되면 id 무시', () => {
|
|
59
|
+
const { scene } = makeMockScene([
|
|
60
|
+
{ refid: 35, id: 'A' },
|
|
61
|
+
{ refid: 99, id: 'B' }
|
|
62
|
+
]);
|
|
63
|
+
const r = findSceneComponent(scene, { refid: 35, id: 'B' });
|
|
64
|
+
expect(r?.refid).toBe(35); // refid 우선
|
|
65
|
+
});
|
|
66
|
+
test('scene 이 null → null', () => {
|
|
67
|
+
expect(findSceneComponent(null, { refid: 1 })).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
test('rootContainer / refidIndexMap 없을 때도 안전', () => {
|
|
70
|
+
expect(findSceneComponent({}, { refid: 1 })).toBeNull();
|
|
71
|
+
expect(findSceneComponent({ rootContainer: {} }, { refid: 1 })).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
test('findById 없을 때 id lookup → null (no throw)', () => {
|
|
74
|
+
expect(findSceneComponent({ rootContainer: {} }, { id: 'X' })).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
test('빈 target → null', () => {
|
|
77
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
78
|
+
expect(findSceneComponent(scene, {})).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
test('id 가 빈 문자열이면 fallback 도 안 함', () => {
|
|
81
|
+
const { scene } = makeMockScene([{ refid: 1, id: '' }]);
|
|
82
|
+
expect(findSceneComponent(scene, { id: '' })).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('dispatchBoardAction — selectComponents', () => {
|
|
86
|
+
test('refid 들 → scene.selected 에 매칭된 컴포넌트 set', () => {
|
|
87
|
+
const { scene } = makeMockScene([
|
|
88
|
+
{ refid: 1 },
|
|
89
|
+
{ refid: 2 },
|
|
90
|
+
{ refid: 3 }
|
|
91
|
+
]);
|
|
92
|
+
const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 3] });
|
|
93
|
+
expect(ok).toBe(true);
|
|
94
|
+
expect(scene.selected).toHaveLength(2);
|
|
95
|
+
expect(scene.selected.map((c) => c.refid).sort()).toEqual([1, 3]);
|
|
96
|
+
});
|
|
97
|
+
test('빈 배열 → scene.selected = [] (deselect)', () => {
|
|
98
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
99
|
+
scene.selected = [{ refid: 1 }];
|
|
100
|
+
const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [] });
|
|
101
|
+
expect(ok).toBe(true);
|
|
102
|
+
expect(scene.selected).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
test('존재하지 않는 refid 는 결과에서 빠지고 나머지는 적용', () => {
|
|
105
|
+
const { scene } = makeMockScene([{ refid: 1 }, { refid: 2 }]);
|
|
106
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 999, 2] });
|
|
107
|
+
expect(scene.selected).toHaveLength(2);
|
|
108
|
+
expect(scene.selected.map((c) => c.refid).sort()).toEqual([1, 2]);
|
|
109
|
+
});
|
|
110
|
+
test('전부 존재하지 않으면 빈 배열로 set (silent — selected 가 비어짐)', () => {
|
|
111
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
112
|
+
scene.selected = [{ refid: 1 }]; // 사전 selection
|
|
113
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: [999] });
|
|
114
|
+
expect(scene.selected).toEqual([]); // 매칭 없음 → deselect
|
|
115
|
+
});
|
|
116
|
+
test('refids 가 배열 아니면 빈 배열로 처리', () => {
|
|
117
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
118
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: 'not-array' });
|
|
119
|
+
expect(scene.selected).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('dispatchBoardAction — centerToComponent', () => {
|
|
123
|
+
test('정상 호출 — scene.centerTo(target, animated=true) default', () => {
|
|
124
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
125
|
+
const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7 });
|
|
126
|
+
expect(ok).toBe(true);
|
|
127
|
+
expect(calls).toHaveLength(1);
|
|
128
|
+
expect(calls[0].method).toBe('centerTo');
|
|
129
|
+
expect(calls[0].target.refid).toBe(7);
|
|
130
|
+
expect(calls[0].animated).toBe(true); // default
|
|
131
|
+
});
|
|
132
|
+
test('animated: false 명시', () => {
|
|
133
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
134
|
+
dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: false });
|
|
135
|
+
expect(calls[0].animated).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
test('animated: true 명시', () => {
|
|
138
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
139
|
+
dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: true });
|
|
140
|
+
expect(calls[0].animated).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
test('존재하지 않는 refid → false 반환, scene.centerTo 미호출', () => {
|
|
143
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
144
|
+
const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 });
|
|
145
|
+
expect(ok).toBe(false);
|
|
146
|
+
expect(calls).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('dispatchBoardAction — fitToView', () => {
|
|
150
|
+
test('mode 미지정 → scene.fit("fit") default', () => {
|
|
151
|
+
const { scene, calls } = makeMockScene();
|
|
152
|
+
const ok = dispatchBoardAction(scene, { action: 'fitToView' });
|
|
153
|
+
expect(ok).toBe(true);
|
|
154
|
+
expect(calls).toEqual([{ method: 'fit', mode: 'fit' }]);
|
|
155
|
+
});
|
|
156
|
+
test('mode 명시 — fit/ratio/width/height 전달', () => {
|
|
157
|
+
for (const mode of ['fit', 'ratio', 'width', 'height']) {
|
|
158
|
+
const { scene, calls } = makeMockScene();
|
|
159
|
+
dispatchBoardAction(scene, { action: 'fitToView', mode });
|
|
160
|
+
expect(calls[0].mode).toBe(mode);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('dispatchBoardAction — setSceneMode', () => {
|
|
165
|
+
test('view → scene.mode = 0', () => {
|
|
166
|
+
const { scene } = makeMockScene();
|
|
167
|
+
scene.mode = 1; // edit
|
|
168
|
+
const ok = dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' });
|
|
169
|
+
expect(ok).toBe(true);
|
|
170
|
+
expect(scene.mode).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
test('edit → scene.mode = 1', () => {
|
|
173
|
+
const { scene } = makeMockScene();
|
|
174
|
+
scene.mode = 0; // view
|
|
175
|
+
dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'edit' });
|
|
176
|
+
expect(scene.mode).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
test('잘못된 mode 값 → view 로 폴백 (edit 이 아니면 0)', () => {
|
|
179
|
+
// 정책: "edit" 외 모든 입력은 view (0). server-side schema 가 enum 으로 1차 차단하지만
|
|
180
|
+
// host 도 방어 — 잘못된 입력에 대해 throw 하지 않고 안전한 default 로.
|
|
181
|
+
const { scene } = makeMockScene();
|
|
182
|
+
scene.mode = 1;
|
|
183
|
+
dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'something' });
|
|
184
|
+
expect(scene.mode).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('dispatchBoardAction — 방어', () => {
|
|
188
|
+
test('scene 이 null → false (no throw)', () => {
|
|
189
|
+
expect(dispatchBoardAction(null, { action: 'fitToView' })).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
test('action 이 null → false', () => {
|
|
192
|
+
const { scene } = makeMockScene();
|
|
193
|
+
expect(dispatchBoardAction(scene, null)).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
test('알 수 없는 action 종류 → false', () => {
|
|
196
|
+
const { scene } = makeMockScene();
|
|
197
|
+
expect(dispatchBoardAction(scene, { action: 'unknownThing' })).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('통합 — 다중 action 시퀀스 (호스트 onBoardActionExecute 패턴)', () => {
|
|
201
|
+
test('centerTo + select + fit 순서대로 적용, 누적 호출 검증', () => {
|
|
202
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
203
|
+
const actions = [
|
|
204
|
+
{ action: 'centerToComponent', refid: 7 },
|
|
205
|
+
{ action: 'selectComponents', refids: [7] },
|
|
206
|
+
{ action: 'fitToView', mode: 'width' }
|
|
207
|
+
];
|
|
208
|
+
for (const a of actions)
|
|
209
|
+
dispatchBoardAction(scene, a);
|
|
210
|
+
// centerTo 와 fit 만 calls 에 누적 (selected 는 직접 할당)
|
|
211
|
+
expect(calls).toHaveLength(2);
|
|
212
|
+
expect(calls[0].method).toBe('centerTo');
|
|
213
|
+
expect(calls[1].method).toBe('fit');
|
|
214
|
+
expect(calls[1].mode).toBe('width');
|
|
215
|
+
expect(scene.selected).toHaveLength(1);
|
|
216
|
+
expect(scene.selected[0].refid).toBe(7);
|
|
217
|
+
});
|
|
218
|
+
test('select 후 setSceneMode — 두 효과 모두 반영', () => {
|
|
219
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
220
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: [1] });
|
|
221
|
+
dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' });
|
|
222
|
+
expect(scene.selected).toHaveLength(1);
|
|
223
|
+
expect(scene.mode).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
test('일부 action 실패 (없는 refid) 해도 후속 action 은 정상 진행', () => {
|
|
226
|
+
const { scene, calls } = makeMockScene([{ refid: 1 }]);
|
|
227
|
+
const ok1 = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 });
|
|
228
|
+
const ok2 = dispatchBoardAction(scene, { action: 'fitToView' });
|
|
229
|
+
expect(ok1).toBe(false);
|
|
230
|
+
expect(ok2).toBe(true);
|
|
231
|
+
expect(calls).toHaveLength(1); // fit 만 호출됨
|
|
232
|
+
expect(calls[0].method).toBe('fit');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
//# sourceMappingURL=board-action-dispatch.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"board-action-dispatch.test.js","sourceRoot":"","sources":["../../client/pages/board-action-dispatch.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-action-dispatch.ts 의 pure helper 회귀 방지.\n *\n * 핵심 보호:\n * - 가짜 scene 객체에 BoardActionOp 던져 실제 things-scene API 호출 인자 검증\n * - findSceneComponent 의 refid 우선 / id fallback 동작\n * - 잘못된 action / 누락 컴포넌트 시 false 반환 (silent fail 차단)\n *\n * AI 가 액션을 만들고 호스트가 things-scene 으로 정확히 매핑하지 않으면 사용자에게는\n * \"AI 응답은 왔는데 화면 그대로\" 회귀로 보임 — 이 layer 가 그 사각지대.\n */\nimport { dispatchBoardAction, findSceneComponent } from './board-action-dispatch'\nimport type { BoardActionOp } from '@things-factory/board-ai'\n\n// ── Mock scene factory ──────────────────────────────────────────────\nfunction makeMockScene(components: Array<{ refid: number; id?: string }> = []) {\n const refidIndexMap = new Map<number, any>()\n for (const c of components) refidIndexMap.set(c.refid, { ...c, model: { ...c } })\n\n const calls: any[] = []\n return {\n scene: {\n mode: 1, // edit\n selected: [] as any[],\n rootContainer: { refidIndexMap },\n findById: (id: string) => {\n for (const c of refidIndexMap.values()) {\n if (c.id === id) return c\n }\n return null\n },\n centerTo(target: any, animated: boolean) {\n calls.push({ method: 'centerTo', target, animated })\n },\n fit(mode: string) {\n calls.push({ method: 'fit', mode })\n }\n } as any,\n calls,\n refidIndexMap\n }\n}\n\ndescribe('findSceneComponent', () => {\n test('refid 매칭 — refidIndexMap 에서 lookup', () => {\n const { scene } = makeMockScene([{ refid: 35, id: 'motor-1' }])\n const r = findSceneComponent(scene, { refid: 35 })\n expect(r?.refid).toBe(35)\n expect(r?.id).toBe('motor-1')\n })\n\n test('refid 가 없는데 매칭 시도 → null', () => {\n const { scene } = makeMockScene([{ refid: 35 }])\n expect(findSceneComponent(scene, { refid: 999 })).toBeNull()\n })\n\n test('id 매칭 — refid 미지정, findById fallback', () => {\n const { scene } = makeMockScene([{ refid: 1, id: 'motor-1' }])\n const r = findSceneComponent(scene, { id: 'motor-1' })\n expect(r?.id).toBe('motor-1')\n })\n\n test('refid 우선 — refid + id 둘 다 주어져도 refid 가 매칭되면 id 무시', () => {\n const { scene } = makeMockScene([\n { refid: 35, id: 'A' },\n { refid: 99, id: 'B' }\n ])\n const r = findSceneComponent(scene, { refid: 35, id: 'B' })\n expect(r?.refid).toBe(35) // refid 우선\n })\n\n test('scene 이 null → null', () => {\n expect(findSceneComponent(null, { refid: 1 })).toBeNull()\n })\n\n test('rootContainer / refidIndexMap 없을 때도 안전', () => {\n expect(findSceneComponent({}, { refid: 1 })).toBeNull()\n expect(findSceneComponent({ rootContainer: {} }, { refid: 1 })).toBeNull()\n })\n\n test('findById 없을 때 id lookup → null (no throw)', () => {\n expect(findSceneComponent({ rootContainer: {} }, { id: 'X' })).toBeNull()\n })\n\n test('빈 target → null', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n expect(findSceneComponent(scene, {})).toBeNull()\n })\n\n test('id 가 빈 문자열이면 fallback 도 안 함', () => {\n const { scene } = makeMockScene([{ refid: 1, id: '' }])\n expect(findSceneComponent(scene, { id: '' })).toBeNull()\n })\n})\n\ndescribe('dispatchBoardAction — selectComponents', () => {\n test('refid 들 → scene.selected 에 매칭된 컴포넌트 set', () => {\n const { scene } = makeMockScene([\n { refid: 1 },\n { refid: 2 },\n { refid: 3 }\n ])\n const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 3] })\n expect(ok).toBe(true)\n expect(scene.selected).toHaveLength(2)\n expect(scene.selected.map((c: any) => c.refid).sort()).toEqual([1, 3])\n })\n\n test('빈 배열 → scene.selected = [] (deselect)', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n scene.selected = [{ refid: 1 }]\n const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [] })\n expect(ok).toBe(true)\n expect(scene.selected).toEqual([])\n })\n\n test('존재하지 않는 refid 는 결과에서 빠지고 나머지는 적용', () => {\n const { scene } = makeMockScene([{ refid: 1 }, { refid: 2 }])\n dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 999, 2] })\n expect(scene.selected).toHaveLength(2)\n expect(scene.selected.map((c: any) => c.refid).sort()).toEqual([1, 2])\n })\n\n test('전부 존재하지 않으면 빈 배열로 set (silent — selected 가 비어짐)', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n scene.selected = [{ refid: 1 }] // 사전 selection\n dispatchBoardAction(scene, { action: 'selectComponents', refids: [999] })\n expect(scene.selected).toEqual([]) // 매칭 없음 → deselect\n })\n\n test('refids 가 배열 아니면 빈 배열로 처리', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n dispatchBoardAction(scene, { action: 'selectComponents', refids: 'not-array' as any })\n expect(scene.selected).toEqual([])\n })\n})\n\ndescribe('dispatchBoardAction — centerToComponent', () => {\n test('정상 호출 — scene.centerTo(target, animated=true) default', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7 })\n expect(ok).toBe(true)\n expect(calls).toHaveLength(1)\n expect(calls[0].method).toBe('centerTo')\n expect(calls[0].target.refid).toBe(7)\n expect(calls[0].animated).toBe(true) // default\n })\n\n test('animated: false 명시', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: false })\n expect(calls[0].animated).toBe(false)\n })\n\n test('animated: true 명시', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: true })\n expect(calls[0].animated).toBe(true)\n })\n\n test('존재하지 않는 refid → false 반환, scene.centerTo 미호출', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 })\n expect(ok).toBe(false)\n expect(calls).toHaveLength(0)\n })\n})\n\ndescribe('dispatchBoardAction — fitToView', () => {\n test('mode 미지정 → scene.fit(\"fit\") default', () => {\n const { scene, calls } = makeMockScene()\n const ok = dispatchBoardAction(scene, { action: 'fitToView' })\n expect(ok).toBe(true)\n expect(calls).toEqual([{ method: 'fit', mode: 'fit' }])\n })\n\n test('mode 명시 — fit/ratio/width/height 전달', () => {\n for (const mode of ['fit', 'ratio', 'width', 'height'] as const) {\n const { scene, calls } = makeMockScene()\n dispatchBoardAction(scene, { action: 'fitToView', mode })\n expect(calls[0].mode).toBe(mode)\n }\n })\n})\n\ndescribe('dispatchBoardAction — setSceneMode', () => {\n test('view → scene.mode = 0', () => {\n const { scene } = makeMockScene()\n scene.mode = 1 // edit\n const ok = dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' })\n expect(ok).toBe(true)\n expect(scene.mode).toBe(0)\n })\n\n test('edit → scene.mode = 1', () => {\n const { scene } = makeMockScene()\n scene.mode = 0 // view\n dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'edit' })\n expect(scene.mode).toBe(1)\n })\n\n test('잘못된 mode 값 → view 로 폴백 (edit 이 아니면 0)', () => {\n // 정책: \"edit\" 외 모든 입력은 view (0). server-side schema 가 enum 으로 1차 차단하지만\n // host 도 방어 — 잘못된 입력에 대해 throw 하지 않고 안전한 default 로.\n const { scene } = makeMockScene()\n scene.mode = 1\n dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'something' as any })\n expect(scene.mode).toBe(0)\n })\n})\n\ndescribe('dispatchBoardAction — 방어', () => {\n test('scene 이 null → false (no throw)', () => {\n expect(dispatchBoardAction(null, { action: 'fitToView' })).toBe(false)\n })\n\n test('action 이 null → false', () => {\n const { scene } = makeMockScene()\n expect(dispatchBoardAction(scene, null as any)).toBe(false)\n })\n\n test('알 수 없는 action 종류 → false', () => {\n const { scene } = makeMockScene()\n expect(\n dispatchBoardAction(scene, { action: 'unknownThing' } as any)\n ).toBe(false)\n })\n})\n\ndescribe('통합 — 다중 action 시퀀스 (호스트 onBoardActionExecute 패턴)', () => {\n test('centerTo + select + fit 순서대로 적용, 누적 호출 검증', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n const actions: BoardActionOp[] = [\n { action: 'centerToComponent', refid: 7 },\n { action: 'selectComponents', refids: [7] },\n { action: 'fitToView', mode: 'width' }\n ]\n for (const a of actions) dispatchBoardAction(scene, a)\n\n // centerTo 와 fit 만 calls 에 누적 (selected 는 직접 할당)\n expect(calls).toHaveLength(2)\n expect(calls[0].method).toBe('centerTo')\n expect(calls[1].method).toBe('fit')\n expect(calls[1].mode).toBe('width')\n expect(scene.selected).toHaveLength(1)\n expect(scene.selected[0].refid).toBe(7)\n })\n\n test('select 후 setSceneMode — 두 효과 모두 반영', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n dispatchBoardAction(scene, { action: 'selectComponents', refids: [1] })\n dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' })\n expect(scene.selected).toHaveLength(1)\n expect(scene.mode).toBe(0)\n })\n\n test('일부 action 실패 (없는 refid) 해도 후속 action 은 정상 진행', () => {\n const { scene, calls } = makeMockScene([{ refid: 1 }])\n const ok1 = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 })\n const ok2 = dispatchBoardAction(scene, { action: 'fitToView' })\n expect(ok1).toBe(false)\n expect(ok2).toBe(true)\n expect(calls).toHaveLength(1) // fit 만 호출됨\n expect(calls[0].method).toBe('fit')\n })\n})\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host 측 BoardEditOp dispatcher — AI 의 model 변경 op 를 things-scene 에 in-place
|
|
3
|
+
* 적용 + revert 용 inverse op 캡처.
|
|
4
|
+
*
|
|
5
|
+
* 핵심:
|
|
6
|
+
* - scene 의 in-place mutation API (scene.add / target.set / scene.remove 등)
|
|
7
|
+
* 로 적용 → commander 가 자동 snapshot push → CMD+Z / dirty / selection 보존.
|
|
8
|
+
* - 'replace' 는 부득이 wholesale 경로 (호스트가 별도 처리). dispatcher 는 받지 않음.
|
|
9
|
+
* - 각 op 의 inverse 를 적용 직전/직후 scene 상태로 계산해 함께 반환 — 호스트가
|
|
10
|
+
* 누적해 두면 나중에 같은 dispatcher 로 역순 적용해 revert 구현.
|
|
11
|
+
*
|
|
12
|
+
* Pure function — Lit element 외부에서 mock scene 으로 단위 테스트 가능.
|
|
13
|
+
*/
|
|
14
|
+
import type { BoardEditOp, ArrangeLayout } from '@things-factory/board-ai';
|
|
15
|
+
export interface DispatchContext {
|
|
16
|
+
/**
|
|
17
|
+
* `add` op 의 component 를 things-scene 의 default 와 deep-merge.
|
|
18
|
+
* 호스트가 BoardModeller 의 template registry 를 보고 채운다 — dispatcher 는 호출만.
|
|
19
|
+
* 미지정 시 component 를 그대로 scene.add 에 전달.
|
|
20
|
+
*/
|
|
21
|
+
normalize?: (c: any) => any;
|
|
22
|
+
}
|
|
23
|
+
export interface DispatchResult {
|
|
24
|
+
/** op 가 실제로 scene 에 반영됐는지. silent no-op (없는 refid 등) → false */
|
|
25
|
+
applied: boolean;
|
|
26
|
+
/** 적용 직전/직후 상태로 계산한 revert 용 inverse — 단일 op 가 다중 inverse 를
|
|
27
|
+
* 생성할 수 있음 (예: align 1개 → modify N개, group 1개 → ungroup 1개). */
|
|
28
|
+
inverseOps: BoardEditOp[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 단일 BoardEditOp 를 things-scene 에 in-place 적용.
|
|
32
|
+
*
|
|
33
|
+
* `replace` 는 받지 않음 (호스트의 wholesale 경로). 미지원 op / 누락 컴포넌트 시
|
|
34
|
+
* `applied: false` 반환. 호스트가 missed 로 분류해 사용자 경고.
|
|
35
|
+
*/
|
|
36
|
+
export declare function dispatchBoardEditOp(scene: any, op: BoardEditOp, ctx?: DispatchContext): DispatchResult;
|
|
37
|
+
/**
|
|
38
|
+
* Sugar layout 위치 계산 — grid / row / column.
|
|
39
|
+
*
|
|
40
|
+
* 정책:
|
|
41
|
+
* - left/top 만 변경. width/height 는 보존 (사용자 의도).
|
|
42
|
+
* - grid: cell size = max(width)/max(height) — 컴포넌트 사이즈 다를 때 겹침 방지.
|
|
43
|
+
* Row-major 채움.
|
|
44
|
+
* - row/column: 각 컴포넌트의 실 size + gap 으로 누적. align (start/center/end) 으로
|
|
45
|
+
* cross-axis 정렬.
|
|
46
|
+
* - anchor 미지정 시 첫 컴포넌트의 현재 (left, top) — 예측 가능.
|
|
47
|
+
*
|
|
48
|
+
* 입력: layout 정의 + 각 target 의 현재 (left, top) (anchor default 용) + sizes.
|
|
49
|
+
* 출력: 각 target 의 새 (left, top). targets 와 동일 순서.
|
|
50
|
+
*
|
|
51
|
+
* Pure function — host dispatcher 외부에서도 단위 테스트 가능.
|
|
52
|
+
*/
|
|
53
|
+
export declare function computeArrangePositions(layout: ArrangeLayout, current: Array<{
|
|
54
|
+
left: number;
|
|
55
|
+
top: number;
|
|
56
|
+
}>, sizes: Array<{
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
}>): Array<{
|
|
60
|
+
left: number;
|
|
61
|
+
top: number;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* scene 의 모든 컴포넌트 refid 수집 — add/group 의 inverse 계산용.
|
|
65
|
+
*
|
|
66
|
+
* scene.add / scene.group 직전·직후 호출해 차집합으로 새 발급 refid 식별.
|
|
67
|
+
*/
|
|
68
|
+
export declare function collectAllRefids(scene: any): number[];
|
|
69
|
+
/**
|
|
70
|
+
* patch 가 변경하려는 키들에 대해 model 의 현재 값을 deep-clone 으로 캡처.
|
|
71
|
+
* inverse modify / modifyBoard op 의 patch 로 사용. 원본에 없던 키는 null 로 보관
|
|
72
|
+
* (revert 시 명시적 null reset).
|
|
73
|
+
*/
|
|
74
|
+
export declare function captureOldKeys(model: any, patch: any): any;
|