@things-factory/board-ai 10.1.24 → 10.1.26
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/PLAN-styling-tools.md +1 -1
- package/dist-client/server/service/assistant.d.ts +5 -4
- package/dist-client/server/service/assistant.js +35 -35
- package/dist-client/server/service/assistant.js.map +1 -1
- package/dist-client/server/service/styling/clear-tools.d.ts +2 -2
- package/dist-client/server/service/styling/clear-tools.js.map +1 -1
- package/dist-client/server/service/styling/copy-tools.d.ts +3 -3
- package/dist-client/server/service/styling/copy-tools.js +1 -1
- package/dist-client/server/service/styling/copy-tools.js.map +1 -1
- package/dist-client/server/service/styling/effect-tools.d.ts +2 -2
- package/dist-client/server/service/styling/effect-tools.js.map +1 -1
- package/dist-client/server/service/styling/fill-tools.d.ts +3 -3
- package/dist-client/server/service/styling/fill-tools.js +1 -1
- package/dist-client/server/service/styling/fill-tools.js.map +1 -1
- package/dist-client/server/service/styling/material-tools.d.ts +2 -2
- package/dist-client/server/service/styling/material-tools.js.map +1 -1
- package/dist-client/server/service/styling/registry.d.ts +6 -6
- package/dist-client/server/service/styling/registry.js +2 -2
- package/dist-client/server/service/styling/registry.js.map +1 -1
- package/dist-client/server/service/styling/stroke-tools.d.ts +2 -2
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -1
- package/dist-client/server/service/styling/text-tools.d.ts +2 -2
- package/dist-client/server/service/styling/text-tools.js.map +1 -1
- package/dist-client/server/service/types.d.ts +6 -148
- package/dist-client/server/service/types.js.map +1 -1
- package/dist-client/server/service/validation/board-model-schema.d.ts +262 -262
- package/dist-client/server/service/validation/board-model-schema.js +4 -4
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -1
- package/dist-client/server/service/validation/tool-validation.js +5 -5
- package/dist-client/server/service/validation/tool-validation.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/index.d.ts +2 -2
- package/dist-server/index.js +2 -2
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/assistant.d.ts +5 -4
- package/dist-server/service/assistant.js +37 -37
- package/dist-server/service/assistant.js.map +1 -1
- package/dist-server/service/board-ai-resolver.js +2 -2
- package/dist-server/service/board-ai-resolver.js.map +1 -1
- package/dist-server/service/index.d.ts +0 -1
- package/dist-server/service/index.js +0 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/patch-entry/board-patch-subscription.js +1 -1
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -1
- package/dist-server/service/patch-entry/patch-entry.d.ts +1 -1
- package/dist-server/service/patch-entry/patch-entry.js +1 -1
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -1
- package/dist-server/service/styling/clear-tools.d.ts +2 -2
- package/dist-server/service/styling/clear-tools.js.map +1 -1
- package/dist-server/service/styling/copy-tools.d.ts +3 -3
- package/dist-server/service/styling/copy-tools.js +1 -1
- package/dist-server/service/styling/copy-tools.js.map +1 -1
- package/dist-server/service/styling/effect-tools.d.ts +2 -2
- package/dist-server/service/styling/effect-tools.js.map +1 -1
- package/dist-server/service/styling/fill-tools.d.ts +3 -3
- package/dist-server/service/styling/fill-tools.js +1 -1
- package/dist-server/service/styling/fill-tools.js.map +1 -1
- package/dist-server/service/styling/material-tools.d.ts +2 -2
- package/dist-server/service/styling/material-tools.js.map +1 -1
- package/dist-server/service/styling/registry.d.ts +6 -6
- package/dist-server/service/styling/registry.js +2 -2
- package/dist-server/service/styling/registry.js.map +1 -1
- package/dist-server/service/styling/stroke-tools.d.ts +2 -2
- package/dist-server/service/styling/stroke-tools.js.map +1 -1
- package/dist-server/service/styling/text-tools.d.ts +2 -2
- package/dist-server/service/styling/text-tools.js.map +1 -1
- package/dist-server/service/types.d.ts +6 -148
- package/dist-server/service/types.js.map +1 -1
- package/dist-server/service/validation/board-model-schema.d.ts +262 -262
- package/dist-server/service/validation/board-model-schema.js +4 -4
- package/dist-server/service/validation/board-model-schema.js.map +1 -1
- package/dist-server/service/validation/tool-validation.js +5 -5
- package/dist-server/service/validation/tool-validation.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json +5 -0
- package/logs/connections-2026-09-17-16.log +0 -0
- package/package.json +8 -7
- package/server/index.ts +2 -2
- package/server/service/assistant-integration.test.ts +44 -44
- package/server/service/assistant-llm-smoke.test.ts +5 -5
- package/server/service/assistant.test.ts +86 -86
- package/server/service/assistant.ts +41 -43
- package/server/service/board-ai-resolver.ts +2 -2
- package/server/service/index.ts +0 -1
- package/server/service/patch-entry/board-patch-subscription.ts +1 -1
- package/server/service/patch-entry/patch-entry.ts +2 -2
- package/server/service/styling/clear-tools.ts +2 -2
- package/server/service/styling/copy-tools.ts +3 -3
- package/server/service/styling/effect-tools.ts +2 -2
- package/server/service/styling/fill-tools.test.ts +1 -1
- package/server/service/styling/fill-tools.ts +4 -4
- package/server/service/styling/material-tools.ts +2 -2
- package/server/service/styling/registry.ts +6 -6
- package/server/service/styling/stroke-tools.test.ts +1 -1
- package/server/service/styling/stroke-tools.ts +3 -3
- package/server/service/styling/text-tools.ts +2 -2
- package/server/service/tool-exposure.test.ts +7 -7
- package/server/service/types.ts +9 -110
- package/server/service/validation/board-model-schema.test.ts +3 -3
- package/server/service/validation/board-model-schema.ts +4 -4
- package/server/service/validation/tool-validation.ts +5 -5
- package/dist-server/service/apply-patch.d.ts +0 -47
- package/dist-server/service/apply-patch.js +0 -369
- package/dist-server/service/apply-patch.js.map +0 -1
- package/server/service/apply-patch-drift.test.ts +0 -831
- package/server/service/apply-patch.test.ts +0 -757
- package/server/service/apply-patch.ts +0 -411
package/server/service/types.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import type { BoardComponent, BoardModel } from '@things-factory/board-import'
|
|
11
11
|
|
|
12
12
|
/* 대화 한 줄은 도메인을 모른다 — 정의는 `@things-factory/ai-assistant` 에 있다(2026-09-10). */
|
|
13
|
+
/*
|
|
14
|
+
* The edit vocabulary is not this package's. A person dragging a box produces the same
|
|
15
|
+
* operations, so they live in @operato/scene-ops with the two appliers that carry them out.
|
|
16
|
+
*/
|
|
17
|
+
import type { SceneActionOp, SceneEditPatch } from '@operato/scene-ops'
|
|
13
18
|
import type { LLMMessage } from '@things-factory/ai-assistant'
|
|
14
19
|
export type { LLMMessage }
|
|
15
20
|
|
|
@@ -121,7 +126,7 @@ import type { AIAnalysisClaim, GroundedAnalysisContract } from '@things-factory/
|
|
|
121
126
|
export type { ToolUsage }
|
|
122
127
|
|
|
123
128
|
/** Board-specific extension of the shared evidence/approval analysis boundary. */
|
|
124
|
-
export interface BoardGroundedAnalysis extends GroundedAnalysisContract<any, AIAnalysisClaim,
|
|
129
|
+
export interface BoardGroundedAnalysis extends GroundedAnalysisContract<any, AIAnalysisClaim, SceneEditPatch | SceneActionOp[]> {
|
|
125
130
|
kind: 'board.analysis.v1'
|
|
126
131
|
}
|
|
127
132
|
|
|
@@ -130,12 +135,12 @@ export interface ChatResponse {
|
|
|
130
135
|
/** 사용자에게 보여줄 텍스트 응답 */
|
|
131
136
|
reply: string
|
|
132
137
|
/** 보드 변경이 있으면 patch */
|
|
133
|
-
patch?:
|
|
138
|
+
patch?: SceneEditPatch
|
|
134
139
|
/**
|
|
135
140
|
* Scene 조작 action (ephemeral — 모델 변경 없음 / undo 영향 없음).
|
|
136
141
|
* 시간순 시퀀스로 호스트가 things-scene API 직접 실행.
|
|
137
142
|
*/
|
|
138
|
-
actions?:
|
|
143
|
+
actions?: SceneActionOp[]
|
|
139
144
|
/** 모호한 입력 시 명확화 질문 (patch 없음) */
|
|
140
145
|
followUp?: string
|
|
141
146
|
/** AI 가 응답 만드는 과정에서 호출한 도구들 (시간순). UI fold-able 박스용. */
|
|
@@ -158,119 +163,13 @@ export interface ChatResponse {
|
|
|
158
163
|
analysis?: BoardGroundedAnalysis
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
export interface BoardEditPatch {
|
|
162
|
-
ops: BoardEditOp[]
|
|
163
|
-
/** 사용자 검수용 1-2 문장 요약 */
|
|
164
|
-
summary: string
|
|
165
|
-
/** 0..1 신뢰도 */
|
|
166
|
-
confidence: number
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 보드 변경 op.
|
|
171
|
-
*
|
|
172
|
-
* 식별자 정책 — 기존 컴포넌트 타깃팅은 `refid` (number) 만 사용.
|
|
173
|
-
* things-scene 의 모든 컴포넌트는 `refid` 를 자동 발급받는다 (universal).
|
|
174
|
-
* `model.id` 는 optional metadata 일 뿐 — 항상 존재하지 않으므로 targeting 에는
|
|
175
|
-
* 부적합. 별개 개념이므로 BoardEditOp / tool 인자에서도 별개 식별자로 분리하지
|
|
176
|
-
* 않고 refid 단일 채널로 일원화.
|
|
177
|
-
*
|
|
178
|
-
* 계층 — 보드는 **최상위 부모** (things-scene 의 model-layer) 이고 그 자체로 자기
|
|
179
|
-
* 속성을 갖는다 (fillStyle, width, height, fitMode, translate, scale, sky, skyColor,
|
|
180
|
-
* exposure, hemi/dirLight 계열, camera 계열). 자식 컴포넌트와 별개. 보드 속성 변경은
|
|
181
|
-
* `modifyBoard`, 자식 변경은 `modify` (refid 기반).
|
|
182
|
-
*
|
|
183
|
-
* 주의 — `name` 은 보드 *엔티티* (DB row) 의 컬럼이지 scene MODEL 의 필드가 아니다.
|
|
184
|
-
* things-scene 의 model-layer 가 인식 안 함. modifyBoard 로 name 을 보내면 model
|
|
185
|
-
* JSON 에 죽은 필드로 박힐 뿐 렌더링/동작에 영향 없음. 보드 라벨 변경은 GraphQL
|
|
186
|
-
* boardPatch (BoardPatch input 의 name 필드) 영역.
|
|
187
|
-
*
|
|
188
|
-
* style 변경 / 이동 / 크기 변경 등 자식 컴포넌트의 변경은 모두 `modify.patch` 안에
|
|
189
|
-
* 흡수. 보드 root 속성 변경은 `modifyBoard.patch` 로.
|
|
190
|
-
*/
|
|
191
|
-
/**
|
|
192
|
-
* Phase 2 — Scene 조작 op 들. 모델 차원 (좌표 / 부모-자식 관계 / z-order) 변경이지만
|
|
193
|
-
* things-scene 의 자체 API (align/distribute/group/ungroup/zorder) 가 일관된 결과를
|
|
194
|
-
* 보장하므로 호스트가 직접 호출. 모델 차원 시뮬레이션 (apply-patch) 은 단순화 —
|
|
195
|
-
* scene 호출 결과가 정본.
|
|
196
|
-
*/
|
|
197
|
-
export type AlignDirection =
|
|
198
|
-
| 'left'
|
|
199
|
-
| 'right'
|
|
200
|
-
| 'center'
|
|
201
|
-
| 'top'
|
|
202
|
-
| 'middle'
|
|
203
|
-
| 'bottom'
|
|
204
|
-
|
|
205
|
-
export type DistributeAxis = 'horizontal' | 'vertical'
|
|
206
|
-
|
|
207
|
-
export type ZorderDirection = 'front' | 'back' | 'forward' | 'backward'
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Sugar layout — `arrange` op 의 layout 종류.
|
|
211
|
-
*
|
|
212
|
-
* 의도: align/distribute 위에 얹는 high-level 의도 표현. AI 가 "3x2 그리드로",
|
|
213
|
-
* "한 줄로", "세로로 일렬" 같은 자연어를 픽셀 노가다 없이 단일 op 로 표현.
|
|
214
|
-
*
|
|
215
|
-
* left/top 만 변경 — width/height 는 유지. AI 가 사이즈도 바꾸려면 별도 modify.
|
|
216
|
-
*
|
|
217
|
-
* 위치 계산은 호스트 (things-scene 측) 가 담당 — 각 컴포넌트의 현재 width/height 를
|
|
218
|
-
* 정확히 알아야 하므로 model 차원 시뮬레이션은 SCENE_ONLY (apply-patch noop).
|
|
219
|
-
*/
|
|
220
|
-
export type ArrangeLayout =
|
|
221
|
-
| { type: 'grid'; cols: number; gap?: number; anchor?: { left: number; top: number } }
|
|
222
|
-
| {
|
|
223
|
-
type: 'row'
|
|
224
|
-
gap?: number
|
|
225
|
-
anchor?: { left: number; top: number }
|
|
226
|
-
align?: 'start' | 'center' | 'end'
|
|
227
|
-
}
|
|
228
|
-
| {
|
|
229
|
-
type: 'column'
|
|
230
|
-
gap?: number
|
|
231
|
-
anchor?: { left: number; top: number }
|
|
232
|
-
align?: 'start' | 'center' | 'end'
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* C-1 — Scene 조작 action 들 (ephemeral). 모델 변경 X, 따라서 BoardEditOp 와 별개
|
|
237
|
-
* 채널 (board-action-execute 이벤트). undo 히스토리 미영향, dirty flag 미영향.
|
|
238
|
-
*
|
|
239
|
-
* 종류:
|
|
240
|
-
* - selectComponents: scene.selected 직접 set (사용자 선택 변경)
|
|
241
|
-
* - centerToComponent: 특정 컴포넌트로 view 이동
|
|
242
|
-
* - fitToView: 보드 전체가 보이도록 fit
|
|
243
|
-
* - setSceneMode: edit / view 모드 전환
|
|
244
|
-
*/
|
|
245
|
-
export type BoardActionOp =
|
|
246
|
-
| { action: 'selectComponents'; refids: number[] }
|
|
247
|
-
| { action: 'centerToComponent'; refid: number; animated?: boolean }
|
|
248
|
-
| { action: 'fitToView'; mode?: 'fit' | 'ratio' | 'width' | 'height' }
|
|
249
|
-
| { action: 'setSceneMode'; mode: 'edit' | 'view' }
|
|
250
|
-
/** 다중 컴포넌트 outline highlight — search/finder UX 의 "이게 모두 매칭이다" 시각화.
|
|
251
|
-
* things-scene 의 highlightSearchResults API 위임 (2D/3D 모두 지원). */
|
|
252
|
-
| { action: 'highlightComponents'; refids: number[] }
|
|
253
|
-
|
|
254
|
-
export type BoardEditOp =
|
|
255
|
-
| { op: 'add'; component: BoardComponent }
|
|
256
|
-
| { op: 'remove'; refid: number }
|
|
257
|
-
| { op: 'modify'; refid: number; patch: Partial<BoardComponent> }
|
|
258
|
-
| { op: 'modifyBoard'; patch: Partial<BoardModel> }
|
|
259
|
-
| { op: 'replace'; board: BoardModel }
|
|
260
|
-
| { op: 'align'; refids: number[]; direction: AlignDirection }
|
|
261
|
-
| { op: 'distribute'; refids: number[]; axis: DistributeAxis }
|
|
262
|
-
| { op: 'group'; refids: number[] }
|
|
263
|
-
| { op: 'ungroup'; refid: number }
|
|
264
|
-
| { op: 'zorder'; refid: number; direction: ZorderDirection }
|
|
265
|
-
| { op: 'arrange'; refids: number[]; layout: ArrangeLayout }
|
|
266
|
-
|
|
267
166
|
/**
|
|
268
167
|
* BoardAIAssistant — 자연어 채팅으로 보드를 다루는 단일 인터페이스.
|
|
269
168
|
*
|
|
270
169
|
* 사용:
|
|
271
170
|
* const ai = new DefaultBoardAIAssistant(baseClient, { scopes: ['fmsim'] })
|
|
272
171
|
* const r = await ai.chat([{ role: 'user', content: 'AGV 3대 추가' }], currentBoard)
|
|
273
|
-
* if (r.patch) currentBoard =
|
|
172
|
+
* if (r.patch) currentBoard = applyScenePatch(currentBoard, r.patch)
|
|
274
173
|
*/
|
|
275
174
|
export interface BoardAIAssistant {
|
|
276
175
|
readonly id: string
|
|
@@ -622,7 +622,7 @@ describe('실 사용자 발견 시나리오 (회귀 방지)', () => {
|
|
|
622
622
|
})
|
|
623
623
|
})
|
|
624
624
|
|
|
625
|
-
describe('BoardPatchSchema —
|
|
625
|
+
describe('BoardPatchSchema — modifyScene 의 patch 검증', () => {
|
|
626
626
|
test('빈 patch 통과 (모든 필드 optional)', () => {
|
|
627
627
|
expect(BoardPatchSchema.safeParse({}).success).toBe(true)
|
|
628
628
|
})
|
|
@@ -650,7 +650,7 @@ describe('BoardPatchSchema — modifyBoard 의 patch 검증', () => {
|
|
|
650
650
|
})
|
|
651
651
|
|
|
652
652
|
test('★ fillStyle 안 gradientStops (stale 이름) — 거부 (FillStyleSchema strict)', () => {
|
|
653
|
-
// 본 사고의 핵심 회귀 방지 —
|
|
653
|
+
// 본 사고의 핵심 회귀 방지 — modifyScene 로 gradientStops 흘러들어오면 catch
|
|
654
654
|
const r = BoardPatchSchema.safeParse({
|
|
655
655
|
fillStyle: {
|
|
656
656
|
type: 'gradient',
|
|
@@ -682,7 +682,7 @@ describe('BoardPatchSchema — modifyBoard 의 patch 검증', () => {
|
|
|
682
682
|
})
|
|
683
683
|
|
|
684
684
|
test('shadow nested unknown key — 거부 (ShadowSchema strict)', () => {
|
|
685
|
-
// 다른 alias leak 시나리오 — setShadow 의 옛 이름 'blur' 가
|
|
685
|
+
// 다른 alias leak 시나리오 — setShadow 의 옛 이름 'blur' 가 modifyScene 를 통해
|
|
686
686
|
// shadow 안에 박히려 할 때 (실제로는 root 에 shadow 가 없지만 검증 흐름 보존)
|
|
687
687
|
const r = BoardPatchSchema.safeParse({
|
|
688
688
|
shadow: { blur: 8, offsetX: 4 } as any
|
|
@@ -336,7 +336,7 @@ export const ComponentSchema = ComponentBaseSchema.superRefine((component, ctx)
|
|
|
336
336
|
* • 식별: id, refid, type
|
|
337
337
|
*
|
|
338
338
|
* ⚠ `name` 은 things-scene model-layer 가 인식 안 함 — Board *엔티티* 의 컬럼이지
|
|
339
|
-
* scene MODEL 의 필드가 아님. AI 가
|
|
339
|
+
* scene MODEL 의 필드가 아님. AI 가 modifyScene 로 name 을 보내도 죽은 필드. 검증
|
|
340
340
|
* 시에도 제거 / 무시 정책.
|
|
341
341
|
*
|
|
342
342
|
* `passthrough` 인 이유 — 3D 광원/카메라/sky 등 키가 너무 많아 strict 로는 회귀 위험
|
|
@@ -384,14 +384,14 @@ export const ComponentPatchSchema = z
|
|
|
384
384
|
.passthrough() // 위치 / 크기 / 컴포넌트 specific 통과
|
|
385
385
|
|
|
386
386
|
/**
|
|
387
|
-
*
|
|
387
|
+
* modifyScene op 의 patch — 보드 root partial.
|
|
388
388
|
*
|
|
389
389
|
* 모든 root 필드 optional (BoardModelSchema 와 달리 width/height 도 optional).
|
|
390
390
|
* passthrough 로 알려지지 않은 root 키 (3D atmosphere, sky, camera 등) 통과.
|
|
391
391
|
* 다만 *알려진 nested object* (fillStyle 등) 는 strict schema 재사용 — 그 안에
|
|
392
392
|
* unknown key 가 들어오면 거부 (예: fillStyle 안 gradientStops 같은 stale 이름).
|
|
393
393
|
*
|
|
394
|
-
* AI 가 setFill 등에서 학습한 vocabulary 를
|
|
394
|
+
* AI 가 setFill 등에서 학습한 vocabulary 를 modifyScene 로 echo 시킬 때 stale
|
|
395
395
|
* key 가 fillStyle / shadow 같은 sub-object 안에 끼면 본 schema 가 catch.
|
|
396
396
|
*/
|
|
397
397
|
export const BoardPatchSchema = z
|
|
@@ -438,7 +438,7 @@ export function validateComponent(component: unknown): ValidationResult {
|
|
|
438
438
|
return { valid: false, errors: formatIssues(r.error) }
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
/**
|
|
441
|
+
/** modifyScene op 의 patch 검증. 보드 root partial. */
|
|
442
442
|
export function validateBoardPatch(patch: unknown): ValidationResult {
|
|
443
443
|
const r = BoardPatchSchema.safeParse(patch)
|
|
444
444
|
if (r.success) return { valid: true }
|
|
@@ -147,7 +147,7 @@ export function validateWriteToolCall(
|
|
|
147
147
|
}
|
|
148
148
|
return { valid: true }
|
|
149
149
|
}
|
|
150
|
-
case '
|
|
150
|
+
case 'modifyScene': {
|
|
151
151
|
// 보드 root partial 검증 — fillStyle/shadow 같은 nested 의 strict schema 가
|
|
152
152
|
// unknown key (e.g. fillStyle.gradientStops 같은 stale 이름) 를 거부.
|
|
153
153
|
// 이게 마지막 방어선 — 다른 styling tool 에서 학습된 vocabulary 가 leak 되면 catch.
|
|
@@ -159,7 +159,7 @@ export function validateWriteToolCall(
|
|
|
159
159
|
return {
|
|
160
160
|
valid: false,
|
|
161
161
|
errors: formatValidationErrors(v),
|
|
162
|
-
suggestion:
|
|
162
|
+
suggestion: modifySceneSuggestion()
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
return { valid: true }
|
|
@@ -174,7 +174,7 @@ export function validateWriteToolCall(
|
|
|
174
174
|
return {
|
|
175
175
|
valid: false,
|
|
176
176
|
errors: formatValidationErrors(v),
|
|
177
|
-
suggestion:
|
|
177
|
+
suggestion: modifySceneSuggestion()
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
return { valid: true }
|
|
@@ -186,9 +186,9 @@ export function validateWriteToolCall(
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
function
|
|
189
|
+
function modifySceneSuggestion(): string {
|
|
190
190
|
return (
|
|
191
|
-
'
|
|
191
|
+
'modifyScene 의 patch 는 things-scene 의 model-layer 가 인식하는 root key 만 사용. ' +
|
|
192
192
|
'fillStyle / strokeStyle / shadow 같은 nested object 의 안쪽 필드는 canonical 이름 (colorStops 등) 정확히 사용. ' +
|
|
193
193
|
'rect 등 자식 컴포넌트 변경은 modifyComponentByRefid 로 분리.'
|
|
194
194
|
)
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BoardEditPatch 를 BoardModel 에 적용하는 표준 helper.
|
|
3
|
-
*
|
|
4
|
-
* 호출자가 patch 응답을 받아 보드에 반영할 때 사용.
|
|
5
|
-
* Pure function — 입력을 mutate 하지 않음.
|
|
6
|
-
*/
|
|
7
|
-
import type { BoardComponent, BoardModel } from '@things-factory/board-import';
|
|
8
|
-
import type { BoardEditOp, BoardEditPatch } from './types.js';
|
|
9
|
-
export interface PatchApplyReport {
|
|
10
|
-
/** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */
|
|
11
|
-
board: BoardModel;
|
|
12
|
-
/** 실제로 보드를 바꾼 op 들. */
|
|
13
|
-
applied: BoardEditOp[];
|
|
14
|
-
/** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */
|
|
15
|
-
missed: BoardEditOp[];
|
|
16
|
-
}
|
|
17
|
-
export declare function applyBoardEditPatch(board: BoardModel | undefined, patch: BoardEditPatch): BoardModel;
|
|
18
|
-
/**
|
|
19
|
-
* Verbose 변형 — 각 op 의 적용 여부를 보고.
|
|
20
|
-
*
|
|
21
|
-
* `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다 (patch 함수의
|
|
22
|
-
* 의도적 단순화). LLM 이 잘못된 id 를 만들어 보내면 사용자에게 "수정했습니다"
|
|
23
|
-
* 라고 답하지만 실제로는 아무 변화도 없는 상황이 발생 — 호스트가 missed 를
|
|
24
|
-
* 보고 사용자에게 경고할 수 있도록 별도 entry point 제공.
|
|
25
|
-
*/
|
|
26
|
-
export declare function applyBoardEditPatchVerbose(board: BoardModel | undefined, patch: BoardEditPatch): PatchApplyReport;
|
|
27
|
-
/**
|
|
28
|
-
* 주어진 board 상태에서 op 의 inverse 를 계산.
|
|
29
|
-
*
|
|
30
|
-
* Revert 기능의 코어 — patch 적용 직전 board 와 op 만 알면 그 op 의 역연산을
|
|
31
|
-
* 만들 수 있으므로, 호스트가 in-place 적용하면서 함께 누적해 두면 나중에
|
|
32
|
-
* 역순 실행만으로 복원 가능.
|
|
33
|
-
*
|
|
34
|
-
* 반환:
|
|
35
|
-
* - inverse op 또는 null (unsupported / 데이터 부족)
|
|
36
|
-
* - add 의 inverse 는 add 후 발급되는 refid 가 필요해서 모델 단계에서 계산 불가 →
|
|
37
|
-
* 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것 (이 함수는 pre-applied
|
|
38
|
-
* board 만 보고 만들 수 있는 종류만 처리: remove / modify / modifyBoard / replace)
|
|
39
|
-
*/
|
|
40
|
-
export declare function computeInverseOp(board: BoardModel | undefined, op: BoardEditOp): BoardEditOp | null;
|
|
41
|
-
export declare function applyOp(board: BoardModel, op: BoardEditOp): BoardModel;
|
|
42
|
-
/**
|
|
43
|
-
* 컴포넌트에 부분 patch 를 적용.
|
|
44
|
-
* threeD 등 nested object 는 deep merge — 호출자가 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
|
|
45
|
-
* patchVal === null 이면 해당 키를 제거한다 (위 시맨틱 참조).
|
|
46
|
-
*/
|
|
47
|
-
export declare function mergeComponent(base: BoardComponent, patch: Partial<BoardComponent>): BoardComponent;
|
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.applyBoardEditPatch = applyBoardEditPatch;
|
|
4
|
-
exports.applyBoardEditPatchVerbose = applyBoardEditPatchVerbose;
|
|
5
|
-
exports.computeInverseOp = computeInverseOp;
|
|
6
|
-
exports.applyOp = applyOp;
|
|
7
|
-
exports.mergeComponent = mergeComponent;
|
|
8
|
-
const EMPTY_BOARD = {
|
|
9
|
-
width: 1000,
|
|
10
|
-
height: 600,
|
|
11
|
-
components: []
|
|
12
|
-
};
|
|
13
|
-
function applyBoardEditPatch(board, patch) {
|
|
14
|
-
return applyBoardEditPatchVerbose(board, patch).board;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Verbose 변형 — 각 op 의 적용 여부를 보고.
|
|
18
|
-
*
|
|
19
|
-
* `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다 (patch 함수의
|
|
20
|
-
* 의도적 단순화). LLM 이 잘못된 id 를 만들어 보내면 사용자에게 "수정했습니다"
|
|
21
|
-
* 라고 답하지만 실제로는 아무 변화도 없는 상황이 발생 — 호스트가 missed 를
|
|
22
|
-
* 보고 사용자에게 경고할 수 있도록 별도 entry point 제공.
|
|
23
|
-
*/
|
|
24
|
-
function applyBoardEditPatchVerbose(board, patch) {
|
|
25
|
-
let result = board ?? EMPTY_BOARD;
|
|
26
|
-
const applied = [];
|
|
27
|
-
const missed = [];
|
|
28
|
-
for (const op of patch.ops) {
|
|
29
|
-
if (SCENE_ONLY_OPS.has(op.op)) {
|
|
30
|
-
// model 차원 noop 이지만 scene 차원 실제 적용 — applied 로 분류.
|
|
31
|
-
applied.push(op);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const next = applyOp(result, op);
|
|
35
|
-
// componentsUnchanged 는 components 배열 동일 + width/height/fillStyle 동일 만 본다.
|
|
36
|
-
// modify / remove 의 silent no-op (ghost refid) 검출 전용 — modifyBoard 가 sky
|
|
37
|
-
// 등 다른 root 키만 바꿀 때 false negative 로 missed 분류되는 회귀 방지.
|
|
38
|
-
const componentMutating = op.op === 'modify' || op.op === 'remove';
|
|
39
|
-
if (next === result || (componentMutating && componentsUnchanged(result, next))) {
|
|
40
|
-
missed.push(op);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
applied.push(op);
|
|
44
|
-
result = next;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return { board: result, applied, missed };
|
|
48
|
-
}
|
|
49
|
-
function componentsUnchanged(prev, next) {
|
|
50
|
-
// applyOp 는 항상 새 객체를 만든다 (`{ ...board, components: ... }`). 따라서 reference
|
|
51
|
-
// 비교가 안 되고 내용 비교가 필요. components 는 map/filter 결과 reference 도 다를 수
|
|
52
|
-
// 있으므로 length + JSON 깊이 비교.
|
|
53
|
-
const a = prev.components ?? [];
|
|
54
|
-
const b = next.components ?? [];
|
|
55
|
-
if (a.length !== b.length)
|
|
56
|
-
return false;
|
|
57
|
-
for (let i = 0; i < a.length; i++) {
|
|
58
|
-
if (a[i] !== b[i])
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
// root meta (width/height/...) 비교
|
|
62
|
-
return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* 주어진 board 상태에서 op 의 inverse 를 계산.
|
|
66
|
-
*
|
|
67
|
-
* Revert 기능의 코어 — patch 적용 직전 board 와 op 만 알면 그 op 의 역연산을
|
|
68
|
-
* 만들 수 있으므로, 호스트가 in-place 적용하면서 함께 누적해 두면 나중에
|
|
69
|
-
* 역순 실행만으로 복원 가능.
|
|
70
|
-
*
|
|
71
|
-
* 반환:
|
|
72
|
-
* - inverse op 또는 null (unsupported / 데이터 부족)
|
|
73
|
-
* - add 의 inverse 는 add 후 발급되는 refid 가 필요해서 모델 단계에서 계산 불가 →
|
|
74
|
-
* 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것 (이 함수는 pre-applied
|
|
75
|
-
* board 만 보고 만들 수 있는 종류만 처리: remove / modify / modifyBoard / replace)
|
|
76
|
-
*/
|
|
77
|
-
function computeInverseOp(board, op) {
|
|
78
|
-
if (!board)
|
|
79
|
-
return null;
|
|
80
|
-
const components = board.components ?? [];
|
|
81
|
-
switch (op.op) {
|
|
82
|
-
case 'add':
|
|
83
|
-
// add 의 inverse 는 새로 발급될 refid 를 알아야 → 호스트 측에서 처리
|
|
84
|
-
return null;
|
|
85
|
-
case 'remove': {
|
|
86
|
-
// 자식 refid 도 매칭 (deep search).
|
|
87
|
-
const target = findComponentDeep(components, op.refid);
|
|
88
|
-
if (!target)
|
|
89
|
-
return null; // 매칭 실패 — silent no-op 이라 inverse 도 없음
|
|
90
|
-
return { op: 'add', component: JSON.parse(JSON.stringify(target)) };
|
|
91
|
-
}
|
|
92
|
-
case 'modify': {
|
|
93
|
-
// 자식 refid 도 매칭 (deep search).
|
|
94
|
-
const target = findComponentDeep(components, op.refid);
|
|
95
|
-
if (!target)
|
|
96
|
-
return null;
|
|
97
|
-
// patch 가 건드린 키만 보관. nested 는 통째로.
|
|
98
|
-
const oldValues = {};
|
|
99
|
-
for (const k of Object.keys(op.patch || {})) {
|
|
100
|
-
const v = target[k];
|
|
101
|
-
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
|
|
102
|
-
}
|
|
103
|
-
return { op: 'modify', refid: op.refid, patch: oldValues };
|
|
104
|
-
}
|
|
105
|
-
case 'modifyBoard': {
|
|
106
|
-
const oldValues = {};
|
|
107
|
-
const patch = op.patch || {};
|
|
108
|
-
for (const k of Object.keys(patch)) {
|
|
109
|
-
if (k === 'components')
|
|
110
|
-
continue;
|
|
111
|
-
const v = board[k];
|
|
112
|
-
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
|
|
113
|
-
}
|
|
114
|
-
return { op: 'modifyBoard', patch: oldValues };
|
|
115
|
-
}
|
|
116
|
-
case 'replace':
|
|
117
|
-
// 이전 보드 통째로 보관 — replace 의 자연스러운 inverse 는 또 다른 replace
|
|
118
|
-
return { op: 'replace', board: JSON.parse(JSON.stringify(board)) };
|
|
119
|
-
case 'zorder': {
|
|
120
|
-
// forward ↔ backward / front ↔ back — 단순 반전. front/back 의 inverse 는
|
|
121
|
-
// 정확하지 않을 수 있음 (front 했다가 back 하면 원래 위치 보장 X), 호스트가
|
|
122
|
-
// 정확한 inverse 를 만들려면 zorder 직전 index 를 캡처해서 명시 modify 시퀀스로.
|
|
123
|
-
// 여기서는 best-effort 만.
|
|
124
|
-
const opp = {
|
|
125
|
-
forward: 'backward',
|
|
126
|
-
backward: 'forward',
|
|
127
|
-
front: 'back',
|
|
128
|
-
back: 'front'
|
|
129
|
-
};
|
|
130
|
-
const dir = opp[op.direction];
|
|
131
|
-
if (!dir)
|
|
132
|
-
return null;
|
|
133
|
-
return { op: 'zorder', refid: op.refid, direction: dir };
|
|
134
|
-
}
|
|
135
|
-
case 'align':
|
|
136
|
-
case 'distribute':
|
|
137
|
-
case 'group':
|
|
138
|
-
case 'ungroup':
|
|
139
|
-
case 'arrange':
|
|
140
|
-
// 좌표 변경 / 부모 재구성 / 새 group refid 발급 등은 scene 호출 후에야
|
|
141
|
-
// 알 수 있음. 호스트가 직접 inverse 만든다 (변경 컴포넌트 좌표 백업 →
|
|
142
|
-
// modify 시퀀스, group 의 새 refid 캡처 → ungroup 등).
|
|
143
|
-
return null;
|
|
144
|
-
default:
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Scene 조작 op 들 — model 차원 시뮬레이션이 things-scene 의 결과와 일치 보장
|
|
150
|
-
* 어려움 (things-scene 의 정확한 좌표 계산 / refid 발급 / 부모 재구성 로직). 따라서
|
|
151
|
-
* apply-patch 에서는 noop. 호스트가 things-scene 의 정본 API (scene.align,
|
|
152
|
-
* scene.distribute, scene.group/ungroup, scene.zorder) 를 직접 호출.
|
|
153
|
-
*
|
|
154
|
-
* verbose 분류에서는 무조건 applied 로 — model 변화가 안 보여도 scene 차원 실제 적용됨.
|
|
155
|
-
*/
|
|
156
|
-
const SCENE_ONLY_OPS = new Set([
|
|
157
|
-
'align',
|
|
158
|
-
'distribute',
|
|
159
|
-
'group',
|
|
160
|
-
'ungroup',
|
|
161
|
-
'zorder',
|
|
162
|
-
'arrange'
|
|
163
|
-
]);
|
|
164
|
-
function applyOp(board, op) {
|
|
165
|
-
const components = board.components ?? [];
|
|
166
|
-
switch (op.op) {
|
|
167
|
-
case 'replace':
|
|
168
|
-
return op.board;
|
|
169
|
-
case 'add':
|
|
170
|
-
// top-level 추가 — 본 op 는 자식 컴포넌트로의 추가는 지원 안 함 (별도 op 필요)
|
|
171
|
-
return { ...board, components: [...components, op.component] };
|
|
172
|
-
case 'remove':
|
|
173
|
-
// 자식 (group/container 안) refid 도 매칭 — deep search.
|
|
174
|
-
return { ...board, components: removeComponentDeep(components, op.refid) };
|
|
175
|
-
case 'modify':
|
|
176
|
-
// 자식 refid 도 매칭 — group/container 안의 컴포넌트 수정 가능.
|
|
177
|
-
return { ...board, components: modifyComponentDeep(components, op.refid, op.patch) };
|
|
178
|
-
case 'modifyBoard': {
|
|
179
|
-
// 루트 속성 (fillStyle / width / height / name 등) 만 deep merge.
|
|
180
|
-
// components 키는 무시 — 자식 변경은 add/remove/modify 별도 op 로.
|
|
181
|
-
const patch = { ...op.patch };
|
|
182
|
-
delete patch.components;
|
|
183
|
-
return mergeBoardRoot(board, patch);
|
|
184
|
-
}
|
|
185
|
-
case 'align':
|
|
186
|
-
case 'distribute':
|
|
187
|
-
case 'group':
|
|
188
|
-
case 'ungroup':
|
|
189
|
-
case 'zorder':
|
|
190
|
-
case 'arrange':
|
|
191
|
-
// scene-only — model 차원 noop. 정본은 things-scene API 가.
|
|
192
|
-
return board;
|
|
193
|
-
default:
|
|
194
|
-
return board;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* patch 내 `null` 의 시맨틱 — **"이 키를 제거하라"**.
|
|
199
|
-
*
|
|
200
|
-
* 이유:
|
|
201
|
-
* 1. fillStyle/strokeStyle 처럼 `string | object | undefined` 인 필드는
|
|
202
|
-
* null 로 set 하면 typeof null === 'object' 함정에 빠져 다운스트림이 크래시.
|
|
203
|
-
* 2. computeInverseOp 가 base 에 키가 없던 (`undefined`) 경우 inverse 를 null
|
|
204
|
-
* 로 발급 — "키 제거" 의도와 정확히 일치. 새 시맨틱 하에서 inverse 가
|
|
205
|
-
* 자연스럽게 올바로 동작.
|
|
206
|
-
* 3. mode 전환 시 stale 필드 정리도 새 patch 에 `oldKey: null` 만 끼워서 처리 가능.
|
|
207
|
-
*
|
|
208
|
-
* 호출자가 명시적으로 "null 값 자체를 set" 하고 싶으면 → 별도 op 를 도입해야 하나,
|
|
209
|
-
* 현재 보드 모델에서 의미 있는 사용처가 없음.
|
|
210
|
-
*/
|
|
211
|
-
function mergeBoardRoot(board, patch) {
|
|
212
|
-
const out = { ...board };
|
|
213
|
-
for (const key of Object.keys(patch)) {
|
|
214
|
-
const bv = board[key];
|
|
215
|
-
const pv = patch[key];
|
|
216
|
-
if (pv === null) {
|
|
217
|
-
delete out[key];
|
|
218
|
-
}
|
|
219
|
-
else if (bv !== null &&
|
|
220
|
-
typeof bv === 'object' &&
|
|
221
|
-
typeof pv === 'object' &&
|
|
222
|
-
!Array.isArray(bv) &&
|
|
223
|
-
!Array.isArray(pv)) {
|
|
224
|
-
out[key] = deepMergeRoot(bv, pv);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
out[key] = pv;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return out;
|
|
231
|
-
}
|
|
232
|
-
function deepMergeRoot(a, b) {
|
|
233
|
-
const out = { ...a };
|
|
234
|
-
for (const key of Object.keys(b)) {
|
|
235
|
-
const av = a[key];
|
|
236
|
-
const bv = b[key];
|
|
237
|
-
if (bv === null) {
|
|
238
|
-
delete out[key];
|
|
239
|
-
}
|
|
240
|
-
else if (av !== null &&
|
|
241
|
-
typeof av === 'object' &&
|
|
242
|
-
typeof bv === 'object' &&
|
|
243
|
-
!Array.isArray(av) &&
|
|
244
|
-
!Array.isArray(bv)) {
|
|
245
|
-
out[key] = deepMergeRoot(av, bv);
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
out[key] = bv;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return out;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* 컴포넌트에 부분 patch 를 적용.
|
|
255
|
-
* threeD 등 nested object 는 deep merge — 호출자가 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
|
|
256
|
-
* patchVal === null 이면 해당 키를 제거한다 (위 시맨틱 참조).
|
|
257
|
-
*/
|
|
258
|
-
function mergeComponent(base, patch) {
|
|
259
|
-
const out = { ...base };
|
|
260
|
-
for (const key of Object.keys(patch)) {
|
|
261
|
-
const baseVal = base[key];
|
|
262
|
-
const patchVal = patch[key];
|
|
263
|
-
if (patchVal === null) {
|
|
264
|
-
delete out[key];
|
|
265
|
-
}
|
|
266
|
-
else if (isPlainObject(baseVal) && isPlainObject(patchVal)) {
|
|
267
|
-
out[key] = deepMerge(baseVal, patchVal);
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
out[key] = patchVal;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return out;
|
|
274
|
-
}
|
|
275
|
-
function deepMerge(a, b) {
|
|
276
|
-
const out = { ...a };
|
|
277
|
-
for (const key of Object.keys(b)) {
|
|
278
|
-
const av = a[key];
|
|
279
|
-
const bv = b[key];
|
|
280
|
-
if (bv === null) {
|
|
281
|
-
delete out[key];
|
|
282
|
-
}
|
|
283
|
-
else if (isPlainObject(av) && isPlainObject(bv)) {
|
|
284
|
-
out[key] = deepMerge(av, bv);
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
out[key] = bv;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
function isPlainObject(v) {
|
|
293
|
-
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
294
|
-
}
|
|
295
|
-
// ── 자식 컴포넌트 (group/container 안) 까지 매칭 — deep search ─
|
|
296
|
-
/**
|
|
297
|
-
* 보드 트리 안에서 refid 로 컴포넌트 찾기 — 깊은 search.
|
|
298
|
-
* top-level 만 보던 옛 동작 → group/container 자식도 매칭.
|
|
299
|
-
*/
|
|
300
|
-
function findComponentDeep(components, refid) {
|
|
301
|
-
for (const c of components) {
|
|
302
|
-
if (!c)
|
|
303
|
-
continue;
|
|
304
|
-
if (c.refid === refid)
|
|
305
|
-
return c;
|
|
306
|
-
const children = c.components;
|
|
307
|
-
if (Array.isArray(children) && children.length > 0) {
|
|
308
|
-
const sub = findComponentDeep(children, refid);
|
|
309
|
-
if (sub)
|
|
310
|
-
return sub;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return undefined;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* modify op 의 deep 적용 — 자식 트리까지 walk 하며 매칭되는 컴포넌트만 mergeComponent.
|
|
317
|
-
* Pure function — 변경 없는 가지는 같은 reference 반환 (structural sharing).
|
|
318
|
-
*/
|
|
319
|
-
function modifyComponentDeep(components, refid, patch) {
|
|
320
|
-
let changed = false;
|
|
321
|
-
const out = components.map(c => {
|
|
322
|
-
if (!c)
|
|
323
|
-
return c;
|
|
324
|
-
if (c.refid === refid) {
|
|
325
|
-
changed = true;
|
|
326
|
-
return mergeComponent(c, patch);
|
|
327
|
-
}
|
|
328
|
-
const children = c.components;
|
|
329
|
-
if (Array.isArray(children) && children.length > 0) {
|
|
330
|
-
const updated = modifyComponentDeep(children, refid, patch);
|
|
331
|
-
if (updated !== children) {
|
|
332
|
-
changed = true;
|
|
333
|
-
return { ...c, components: updated };
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return c;
|
|
337
|
-
});
|
|
338
|
-
return changed ? out : components;
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* remove op 의 deep 적용 — 자식 트리까지 walk 하며 해당 refid 의 컴포넌트 제거.
|
|
342
|
-
* 부모는 보존, 빈 group 도 보존 (group 이 비더라도 사용자가 의도적으로 만든 컨테이너일 수 있음).
|
|
343
|
-
*/
|
|
344
|
-
function removeComponentDeep(components, refid) {
|
|
345
|
-
let changed = false;
|
|
346
|
-
const out = [];
|
|
347
|
-
for (const c of components) {
|
|
348
|
-
if (!c) {
|
|
349
|
-
out.push(c);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
if (c.refid === refid) {
|
|
353
|
-
changed = true;
|
|
354
|
-
continue; // 이 컴포넌트 자체를 제거
|
|
355
|
-
}
|
|
356
|
-
const children = c.components;
|
|
357
|
-
if (Array.isArray(children) && children.length > 0) {
|
|
358
|
-
const updated = removeComponentDeep(children, refid);
|
|
359
|
-
if (updated !== children) {
|
|
360
|
-
changed = true;
|
|
361
|
-
out.push({ ...c, components: updated });
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
out.push(c);
|
|
366
|
-
}
|
|
367
|
-
return changed ? out : components;
|
|
368
|
-
}
|
|
369
|
-
//# sourceMappingURL=apply-patch.js.map
|