@things-factory/board-ai 10.0.0-beta.108
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 +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 보드의 컴포넌트 트리 walk helper.
|
|
3
|
+
*
|
|
4
|
+
* things-scene 의 보드 모델은 group / container 같은 부모 컴포넌트가 자기
|
|
5
|
+
* `components` 배열로 자식을 갖는 *재귀 구조*. AI / styling tool 의 입장에선
|
|
6
|
+
* "어떤 컴포넌트가 어디에 있든" 동일하게 다뤄야 함 — depth 와 parent 정보만
|
|
7
|
+
* 메타로 첨부해 *평면 list* 로 펼쳐 노출.
|
|
8
|
+
*
|
|
9
|
+
* 본 모듈은 *순수* 함수만 — board model 입력만으로 결과 결정. mutation 없음.
|
|
10
|
+
*/
|
|
11
|
+
import type { BoardComponent, BoardModel } from '@things-factory/board-import'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 평면화된 컴포넌트 — 원본 컴포넌트 + 메타 (parent, depth, path).
|
|
15
|
+
*
|
|
16
|
+
* 메타는 원본을 mutate 하지 않고 별도로 노출 — LLM 이 "어느 group 안인지" 알
|
|
17
|
+
* 수 있되 영속 데이터엔 영향 없음.
|
|
18
|
+
*/
|
|
19
|
+
export interface FlatComponent {
|
|
20
|
+
/** 원본 컴포넌트 (참조 — 변경 시 보드에 반영). */
|
|
21
|
+
component: BoardComponent
|
|
22
|
+
/** 컴포넌트의 refid (없으면 undefined). 타깃팅 용도. */
|
|
23
|
+
refid: number | undefined
|
|
24
|
+
/** 부모 컴포넌트의 refid (root 직속이면 undefined). */
|
|
25
|
+
parentRefid: number | undefined
|
|
26
|
+
/** 0=root 직속, 1=group 안, 2=group 안 group 안, ... */
|
|
27
|
+
depth: number
|
|
28
|
+
/** 부모-자식 chain 의 refid 시퀀스 (root 부터). 본 컴포넌트의 refid 도 끝에 포함. */
|
|
29
|
+
path: number[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 보드의 모든 컴포넌트를 재귀 walk 해 평면 list 로 반환.
|
|
34
|
+
*
|
|
35
|
+
* 순서: depth-first pre-order — 부모 먼저, 그 다음 자식. 사용자/AI 가 보드를
|
|
36
|
+
* 좌상단부터 시각적으로 읽는 순서와 일치.
|
|
37
|
+
*
|
|
38
|
+
* 재귀 깊이 안전 — 일반적인 보드 (depth < 10) 에서 stack overflow 없음.
|
|
39
|
+
* 비정상적으로 깊은 (cycle 가능성?) 케이스는 maxDepth 로 cap.
|
|
40
|
+
*/
|
|
41
|
+
export function collectAllComponents(
|
|
42
|
+
board: BoardModel | null | undefined,
|
|
43
|
+
options: { maxDepth?: number } = {}
|
|
44
|
+
): FlatComponent[] {
|
|
45
|
+
if (!board || !Array.isArray(board.components)) return []
|
|
46
|
+
const maxDepth = options.maxDepth ?? 32
|
|
47
|
+
const out: FlatComponent[] = []
|
|
48
|
+
walk(board.components, undefined, 0, [], maxDepth, out)
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function walk(
|
|
53
|
+
components: BoardComponent[],
|
|
54
|
+
parentRefid: number | undefined,
|
|
55
|
+
depth: number,
|
|
56
|
+
parentPath: number[],
|
|
57
|
+
maxDepth: number,
|
|
58
|
+
out: FlatComponent[]
|
|
59
|
+
): void {
|
|
60
|
+
if (depth > maxDepth) return
|
|
61
|
+
for (const c of components) {
|
|
62
|
+
if (!c || typeof c !== 'object') continue
|
|
63
|
+
const refid = typeof (c as any).refid === 'number' ? (c as any).refid : undefined
|
|
64
|
+
const path = refid !== undefined ? [...parentPath, refid] : parentPath
|
|
65
|
+
out.push({ component: c, refid, parentRefid, depth, path })
|
|
66
|
+
|
|
67
|
+
// 자식 컴포넌트 — group / container 의 components 배열
|
|
68
|
+
const children = (c as any).components
|
|
69
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
70
|
+
walk(children, refid, depth + 1, path, maxDepth, out)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 보드 트리 안에서 refid 로 컴포넌트 찾기 — 깊은 search.
|
|
77
|
+
*
|
|
78
|
+
* apply-patch 의 modify / remove 가 자식 refid 도 매칭하도록 해주는 핵심 helper.
|
|
79
|
+
* top-level 만 보던 기존 동작과 달리 group 안 자식도 찾는다.
|
|
80
|
+
*
|
|
81
|
+
* 반환: 찾은 컴포넌트 + 그 컴포넌트가 들어있는 array (parent 또는 board.components)
|
|
82
|
+
* + array 내 index. parent 정보로 in-place mutation 가능.
|
|
83
|
+
*/
|
|
84
|
+
export interface FoundComponent {
|
|
85
|
+
component: BoardComponent
|
|
86
|
+
/** 본 컴포넌트가 속한 array (부모의 components 또는 board.components). 직접 mutation 가능. */
|
|
87
|
+
parentArray: BoardComponent[]
|
|
88
|
+
/** parentArray 내에서의 index. */
|
|
89
|
+
index: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function findComponentByRefid(
|
|
93
|
+
board: BoardModel | null | undefined,
|
|
94
|
+
refid: number
|
|
95
|
+
): FoundComponent | undefined {
|
|
96
|
+
if (!board || !Array.isArray(board.components)) return undefined
|
|
97
|
+
return findInArray(board.components, refid)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findInArray(
|
|
101
|
+
arr: BoardComponent[],
|
|
102
|
+
refid: number
|
|
103
|
+
): FoundComponent | undefined {
|
|
104
|
+
for (let i = 0; i < arr.length; i++) {
|
|
105
|
+
const c = arr[i]
|
|
106
|
+
if (!c || typeof c !== 'object') continue
|
|
107
|
+
if ((c as any).refid === refid) {
|
|
108
|
+
return { component: c, parentArray: arr, index: i }
|
|
109
|
+
}
|
|
110
|
+
const children = (c as any).components
|
|
111
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
112
|
+
const sub = findInArray(children, refid)
|
|
113
|
+
if (sub) return sub
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* collectAllComponents 결과를 LLM 표시용으로 가볍게 요약 — 메타만.
|
|
121
|
+
* 본 컴포넌트 데이터는 별도 summarize 단계에서 처리.
|
|
122
|
+
*/
|
|
123
|
+
export function describeComponentMeta(flat: FlatComponent): {
|
|
124
|
+
refid: number | undefined
|
|
125
|
+
type: string
|
|
126
|
+
parentRefid: number | undefined
|
|
127
|
+
depth: number
|
|
128
|
+
} {
|
|
129
|
+
return {
|
|
130
|
+
refid: flat.refid,
|
|
131
|
+
type: typeof flat.component.type === 'string' ? flat.component.type : 'unknown',
|
|
132
|
+
parentRefid: flat.parentRefid,
|
|
133
|
+
depth: flat.depth
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BoardAIChatResolver } from './board-ai-resolver.js'
|
|
2
|
+
import { ChatSessionResolver } from './chat-session-resolver.js'
|
|
3
|
+
import { BoardPatchSubscription } from './patch-entry/board-patch-subscription.js'
|
|
4
|
+
import { BoardChatMessageSubscription } from './chat-message/chat-message-subscription.js'
|
|
5
|
+
import { entities as ChatSessionEntities } from './chat-session/index.js'
|
|
6
|
+
import { entities as ChatMessageEntities } from './chat-message/index.js'
|
|
7
|
+
import { entities as ChatSessionParticipantEntities } from './chat-session-participant/index.js'
|
|
8
|
+
import { entities as PatchEntryEntities } from './patch-entry/index.js'
|
|
9
|
+
|
|
10
|
+
export * from './types.js'
|
|
11
|
+
export * from './assistant.js'
|
|
12
|
+
export * from './apply-patch.js'
|
|
13
|
+
export * from './prefab-registry.js'
|
|
14
|
+
export * from './mention-resolver.js'
|
|
15
|
+
export * from './catalog-resolver.js'
|
|
16
|
+
export * from './board-ai-resolver.js'
|
|
17
|
+
export * from './chat-session-resolver.js'
|
|
18
|
+
export * from './chat-session/index.js'
|
|
19
|
+
export * from './chat-message/index.js'
|
|
20
|
+
export * from './chat-session-participant/index.js'
|
|
21
|
+
export * from './patch-entry/index.js'
|
|
22
|
+
|
|
23
|
+
export const entities = [
|
|
24
|
+
...ChatSessionEntities,
|
|
25
|
+
...ChatMessageEntities,
|
|
26
|
+
...ChatSessionParticipantEntities,
|
|
27
|
+
...PatchEntryEntities
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export const schema = {
|
|
31
|
+
resolverClasses: [BoardAIChatResolver, ChatSessionResolver, BoardPatchSubscription, BoardChatMessageSubscription]
|
|
32
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extractMentionUserIds — 단위 테스트 (초대 트리거 핵심).
|
|
3
|
+
* content 의 @user{userId:UUID} 마커에서 userId 집합 추출.
|
|
4
|
+
*/
|
|
5
|
+
import { extractMentionUserIds, newParticipantUserIds } from './mention-marker'
|
|
6
|
+
|
|
7
|
+
describe('extractMentionUserIds', () => {
|
|
8
|
+
it('단일 @user 멘션', () => {
|
|
9
|
+
expect(extractMentionUserIds('@영희{userId:u-1} 봐줘')).toEqual(['u-1'])
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('복수 멘션 — 순서 보존', () => {
|
|
13
|
+
expect(extractMentionUserIds('@a{userId:u-1} @b{userId:u-2}')).toEqual(['u-1', 'u-2'])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('중복 userId 제거 (같은 사람 여러 번)', () => {
|
|
17
|
+
expect(extractMentionUserIds('@a{userId:u-1} ... @a{userId:u-1}')).toEqual(['u-1'])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('refid 마커는 무시 (userId 만)', () => {
|
|
21
|
+
expect(extractMentionUserIds('#모터{refid:35} @영희{userId:u-9}')).toEqual(['u-9'])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('마커 없는 평문/빈/널 → 빈 배열', () => {
|
|
25
|
+
expect(extractMentionUserIds('그냥 @영희 (마커 없음)')).toEqual([])
|
|
26
|
+
expect(extractMentionUserIds('')).toEqual([])
|
|
27
|
+
expect(extractMentionUserIds(null)).toEqual([])
|
|
28
|
+
expect(extractMentionUserIds(undefined)).toEqual([])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('UUID 형식 userId', () => {
|
|
32
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000'
|
|
33
|
+
expect(extractMentionUserIds(`@kim{userId:${uuid}}`)).toEqual([uuid])
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('newParticipantUserIds (초대 차집합)', () => {
|
|
38
|
+
it('기존 참여자 제외', () => {
|
|
39
|
+
expect(newParticipantUserIds(['u1', 'u2', 'u3'], ['u2'])).toEqual(['u1', 'u3'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('자기 자신(inviter) 제외 — 자기 멘션으로 자가초대 안 함', () => {
|
|
43
|
+
expect(newParticipantUserIds(['u1', 'me'], [], 'me')).toEqual(['u1'])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('멘션 내 중복 제거', () => {
|
|
47
|
+
expect(newParticipantUserIds(['u1', 'u1', 'u2'], [])).toEqual(['u1', 'u2'])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('모두 기존이면 빈 배열 (재초대 불필요)', () => {
|
|
51
|
+
expect(newParticipantUserIds(['u1', 'u2'], ['u1', 'u2'])).toEqual([])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('빈 멘션 → 빈 배열', () => {
|
|
55
|
+
expect(newParticipantUserIds([], ['u1'], 'me')).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('falsy userId 무시', () => {
|
|
59
|
+
expect(newParticipantUserIds(['', 'u1'], [])).toEqual(['u1'])
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mention-marker — `#token{refid:N}` inline marker 단위 테스트.
|
|
3
|
+
*
|
|
4
|
+
* 핵심: 단일 source-of-truth (content 컬럼 한 곳) 보존, idempotent 주입,
|
|
5
|
+
* LLM 입력용 strip, AI 응답 자동 resolve + 주입.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
stripMentionRefids,
|
|
9
|
+
injectMentionRefids,
|
|
10
|
+
injectMentionUserIds,
|
|
11
|
+
injectAiMentionRefids,
|
|
12
|
+
MENTION_MARKER_RE
|
|
13
|
+
} from './mention-marker'
|
|
14
|
+
|
|
15
|
+
const make = (components: any[] = []) => ({ width: 1000, height: 600, components })
|
|
16
|
+
|
|
17
|
+
describe('stripMentionRefids — marker 제거', () => {
|
|
18
|
+
test('단일 marker 제거', () => {
|
|
19
|
+
expect(stripMentionRefids('#motor1{refid:35} 빨갛게')).toBe('#motor1 빨갛게')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('다중 marker 제거', () => {
|
|
23
|
+
expect(stripMentionRefids('#a{refid:1} 와 #b{refid:2}')).toBe('#a 와 #b')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('marker 없으면 원본 그대로', () => {
|
|
27
|
+
expect(stripMentionRefids('#motor1 빨갛게')).toBe('#motor1 빨갛게')
|
|
28
|
+
expect(stripMentionRefids('마커 없는 텍스트')).toBe('마커 없는 텍스트')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('빈/널 안전', () => {
|
|
32
|
+
expect(stripMentionRefids('')).toBe('')
|
|
33
|
+
expect(stripMentionRefids(null)).toBe('')
|
|
34
|
+
expect(stripMentionRefids(undefined)).toBe('')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('정규식 reset — 연속 호출 시 안전', () => {
|
|
38
|
+
// global 정규식이지만 .replace 는 lastIndex 영향 없음
|
|
39
|
+
const r1 = stripMentionRefids('#a{refid:1}')
|
|
40
|
+
const r2 = stripMentionRefids('#b{refid:2}')
|
|
41
|
+
expect(r1).toBe('#a')
|
|
42
|
+
expect(r2).toBe('#b')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('injectMentionRefids — popup picks 를 본문에 marker 주입', () => {
|
|
47
|
+
test('단일 token 주입', () => {
|
|
48
|
+
expect(
|
|
49
|
+
injectMentionRefids('#motor1 빨갛게', [{ token: 'motor1', refid: 35 }])
|
|
50
|
+
).toBe('#motor1{refid:35} 빨갛게')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('다중 token — 각각 주입', () => {
|
|
54
|
+
expect(
|
|
55
|
+
injectMentionRefids('#a 와 #b', [
|
|
56
|
+
{ token: 'a', refid: 10 },
|
|
57
|
+
{ token: 'b', refid: 20 }
|
|
58
|
+
])
|
|
59
|
+
).toBe('#a{refid:10} 와 #b{refid:20}')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('미등록 token 은 건드리지 않음', () => {
|
|
63
|
+
expect(
|
|
64
|
+
injectMentionRefids('#motor1 와 #other', [{ token: 'motor1', refid: 35 }])
|
|
65
|
+
).toBe('#motor1{refid:35} 와 #other')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('이미 marker 있으면 덮어쓰기 안 함 (idempotent)', () => {
|
|
69
|
+
expect(
|
|
70
|
+
injectMentionRefids('#motor1{refid:99}', [{ token: 'motor1', refid: 35 }])
|
|
71
|
+
).toBe('#motor1{refid:99}')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('단어 중간 # 매칭 안 함', () => {
|
|
75
|
+
expect(
|
|
76
|
+
injectMentionRefids('foo#motor1', [{ token: 'motor1', refid: 35 }])
|
|
77
|
+
).toBe('foo#motor1')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('빈 picks / 빈 텍스트 안전', () => {
|
|
81
|
+
expect(injectMentionRefids('#a', [])).toBe('#a')
|
|
82
|
+
expect(injectMentionRefids('', [{ token: 'a', refid: 1 }])).toBe('')
|
|
83
|
+
expect(injectMentionRefids('#a', null)).toBe('#a')
|
|
84
|
+
expect(injectMentionRefids('#a', undefined)).toBe('#a')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('한글 token 주입', () => {
|
|
88
|
+
expect(
|
|
89
|
+
injectMentionRefids('#경광등1 빨갛게', [{ token: '경광등1', refid: 42 }])
|
|
90
|
+
).toBe('#경광등1{refid:42} 빨갛게')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('동일 token 다중 등장 — 모두 주입', () => {
|
|
94
|
+
expect(
|
|
95
|
+
injectMentionRefids('#x 그리고 #x', [{ token: 'x', refid: 7 }])
|
|
96
|
+
).toBe('#x{refid:7} 그리고 #x{refid:7}')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('injectAiMentionRefids — AI 응답 #token resolve + 주입', () => {
|
|
101
|
+
test('id 매칭으로 marker 주입', () => {
|
|
102
|
+
const board = make([{ refid: 35, type: 'rect', id: 'motor1' }])
|
|
103
|
+
expect(injectAiMentionRefids('#motor1 을 빨갛게 만들었습니다', board)).toBe(
|
|
104
|
+
'#motor1{refid:35} 을 빨갛게 만들었습니다'
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('한글 name 매칭', () => {
|
|
109
|
+
const board = make([{ refid: 42, type: 'rect', name: '경광등1' }])
|
|
110
|
+
expect(injectAiMentionRefids('#경광등1 위치 변경', board)).toBe(
|
|
111
|
+
'#경광등1{refid:42} 위치 변경'
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('매칭 안 되는 token — marker 없이 통과', () => {
|
|
116
|
+
const board = make([{ refid: 1, type: 'rect', id: 'a' }])
|
|
117
|
+
expect(injectAiMentionRefids('#a 처리, #unknown 못 찾음', board)).toBe(
|
|
118
|
+
'#a{refid:1} 처리, #unknown 못 찾음'
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('빈 보드 — 변경 없음', () => {
|
|
123
|
+
expect(injectAiMentionRefids('#a 처리', make([]))).toBe('#a 처리')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('null/undefined board 안전', () => {
|
|
127
|
+
expect(injectAiMentionRefids('#a', null)).toBe('#a')
|
|
128
|
+
expect(injectAiMentionRefids('#a', undefined)).toBe('#a')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('동일 token 여러 번 — 모두 주입', () => {
|
|
132
|
+
const board = make([{ refid: 7, type: 'rect', id: 'x' }])
|
|
133
|
+
expect(injectAiMentionRefids('#x 와 #x 처리', board)).toBe(
|
|
134
|
+
'#x{refid:7} 와 #x{refid:7} 처리'
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('이미 marker 있으면 그대로 (이중 처리 방지)', () => {
|
|
139
|
+
const board = make([{ refid: 35, type: 'rect', id: 'motor1' }])
|
|
140
|
+
expect(injectAiMentionRefids('#motor1{refid:99} 처리', board)).toBe(
|
|
141
|
+
'#motor1{refid:99} 처리'
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('inject + strip 왕복 — round-trip 무결성', () => {
|
|
147
|
+
test('inject 후 strip → 원본 텍스트 (마커 없는 입력)', () => {
|
|
148
|
+
const original = '#motor1 빨갛게 #pump 이동'
|
|
149
|
+
const enriched = injectMentionRefids(original, [
|
|
150
|
+
{ token: 'motor1', refid: 35 },
|
|
151
|
+
{ token: 'pump', refid: 42 }
|
|
152
|
+
])
|
|
153
|
+
expect(stripMentionRefids(enriched)).toBe(original)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('strip 후 다시 inject — 같은 picks 면 동일 결과', () => {
|
|
157
|
+
const enriched = '#a{refid:1} 와 #b{refid:2}'
|
|
158
|
+
const stripped = stripMentionRefids(enriched)
|
|
159
|
+
const reInjected = injectMentionRefids(stripped, [
|
|
160
|
+
{ token: 'a', refid: 1 },
|
|
161
|
+
{ token: 'b', refid: 2 }
|
|
162
|
+
])
|
|
163
|
+
expect(reInjected).toBe(enriched)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('injectMentionUserIds — `@user{userId:UUID}` 주입', () => {
|
|
168
|
+
test('단일 user 주입', () => {
|
|
169
|
+
expect(
|
|
170
|
+
injectMentionUserIds('@경철 도와줘', [{ token: '경철', userId: 'abc-123' }])
|
|
171
|
+
).toBe('@경철{userId:abc-123} 도와줘')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('이미 marker 있으면 idempotent', () => {
|
|
175
|
+
expect(
|
|
176
|
+
injectMentionUserIds('@경철{userId:old-id} 처리', [
|
|
177
|
+
{ token: '경철', userId: 'new-id' }
|
|
178
|
+
])
|
|
179
|
+
).toBe('@경철{userId:old-id} 처리')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('# 토큰은 영향 없음 (다른 trigger)', () => {
|
|
183
|
+
expect(
|
|
184
|
+
injectMentionUserIds('#motor1 와 @경철 같이', [
|
|
185
|
+
{ token: 'motor1', userId: 'wrong-id' },
|
|
186
|
+
{ token: '경철', userId: 'abc' }
|
|
187
|
+
])
|
|
188
|
+
).toBe('#motor1 와 @경철{userId:abc} 같이')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('빈 picks / 빈 텍스트 안전', () => {
|
|
192
|
+
expect(injectMentionUserIds('@경철', [])).toBe('@경철')
|
|
193
|
+
expect(injectMentionUserIds('', [{ token: 'x', userId: 'a' }])).toBe('')
|
|
194
|
+
expect(injectMentionUserIds('@경철', null)).toBe('@경철')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('단어 중간 @ 매칭 안 함', () => {
|
|
198
|
+
expect(
|
|
199
|
+
injectMentionUserIds('user@host', [{ token: 'host', userId: 'x' }])
|
|
200
|
+
).toBe('user@host')
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('stripMentionRefids — 다양한 마커 키 일반화 처리', () => {
|
|
205
|
+
test('refid + userId 마커 모두 strip', () => {
|
|
206
|
+
expect(
|
|
207
|
+
stripMentionRefids('#motor{refid:35} 와 @경철{userId:abc-123} 처리')
|
|
208
|
+
).toBe('#motor 와 @경철 처리')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('미래 마커 키도 strip — `{key:value}` 형식이면 모두', () => {
|
|
212
|
+
expect(stripMentionRefids('@bot{agentId:claude-1} hi')).toBe('@bot hi')
|
|
213
|
+
expect(stripMentionRefids('test{threadId:t1}done')).toBe('testdone')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('MENTION_MARKER_RE — 정규식 직접 검증', () => {
|
|
218
|
+
test('단일 매칭', () => {
|
|
219
|
+
const m = '#motor1{refid:35}'.match(/\{refid:(\d+)\}/)
|
|
220
|
+
expect(m?.[1]).toBe('35')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('global flag — exec lastIndex 영향 받음 (호출 시 새 RegExp 권장)', () => {
|
|
224
|
+
// MENTION_MARKER_RE 자체가 g flag 라 exec 사용 시 주의 — replace 권장
|
|
225
|
+
const re = MENTION_MARKER_RE
|
|
226
|
+
expect(re.flags).toContain('g')
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `#token{refid:N}` inline marker — 채팅 메시지의 단일 source-of-truth.
|
|
3
|
+
*
|
|
4
|
+
* 설계 원칙:
|
|
5
|
+
* - 메시지 영속은 `ChatMessage.content` 한 컬럼에 통합. token-refid 매핑은 본문 안에
|
|
6
|
+
* `#motor1{refid:35}` 형태로 inline 마킹.
|
|
7
|
+
* - 별도 컬럼이 없으니 두 데이터 사이 consistency 깨질 일 자체가 없음.
|
|
8
|
+
* - LLM 한테는 `stripMentionRefids` 로 마커 제거 후 보냄 — 토큰 낭비 / 주의 분산 차단.
|
|
9
|
+
* - 클라이언트 렌더는 `#token{refid:N}` 패턴을 추출해 `<span data-refid="N">#token</span>`
|
|
10
|
+
* 으로 변환 → 클릭 시 deterministic 한 컴포넌트 점프.
|
|
11
|
+
*
|
|
12
|
+
* 마커 형식:
|
|
13
|
+
* - `#token{refid:N}` (N 은 정수)
|
|
14
|
+
* - token 과 `{` 사이 공백 없음
|
|
15
|
+
* - `{refid:N}` 자체는 사람도 디버그 시 읽기 쉬움 (zero-width 문자나 prefix 약어
|
|
16
|
+
* 보다 우선) — 렌더링 깨졌을 때 로그 / DB 직접 보고 파악 가능
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { BoardModel } from '@things-factory/board-import'
|
|
20
|
+
import { findMentions, type MentionPick } from './mention-resolver'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 인라인 마커 정규식 — `{key:value}` 형태. 현재 사용:
|
|
24
|
+
* - `{refid:N}` — `#component` 멘션 (things-scene refid)
|
|
25
|
+
* - `{userId:UUID}` — `@user` 멘션 (도메인 사용자)
|
|
26
|
+
* 향후 새 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식으로 추가 가능.
|
|
27
|
+
*/
|
|
28
|
+
export const MENTION_MARKER_RE = /\{[a-zA-Z][a-zA-Z0-9]*:[^}\s]+\}/g
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 멘션된 userId 중 *아직 참여자가 아닌* + *자기 자신이 아닌* 신규만 (C4 초대 — 차집합).
|
|
32
|
+
* 순수 — 누구를 새로 참여자 등록할지 판정. (resolver 가 repo 작업과 분리해 테스트 가능.)
|
|
33
|
+
*/
|
|
34
|
+
export function newParticipantUserIds(
|
|
35
|
+
mentioned: ReadonlyArray<string>,
|
|
36
|
+
existing: ReadonlyArray<string>,
|
|
37
|
+
selfUserId?: string | null
|
|
38
|
+
): string[] {
|
|
39
|
+
const have = new Set(existing)
|
|
40
|
+
if (selfUserId) have.add(selfUserId)
|
|
41
|
+
const out: string[] = []
|
|
42
|
+
const seen = new Set<string>()
|
|
43
|
+
for (const id of mentioned) {
|
|
44
|
+
if (!id || have.has(id) || seen.has(id)) continue
|
|
45
|
+
seen.add(id)
|
|
46
|
+
out.push(id)
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 본문에서 `@user{userId:UUID}` 마커의 *userId 집합* 추출 (C4 — 초대 트리거).
|
|
53
|
+
* 같은 user 중복 제거, 순서 보존(첫 등장순). 마커 없으면 빈 배열.
|
|
54
|
+
* 순수 — content 만으로 "이 메시지가 누구를 멘션했나" 판정.
|
|
55
|
+
*/
|
|
56
|
+
export function extractMentionUserIds(text: string | null | undefined): string[] {
|
|
57
|
+
if (!text) return []
|
|
58
|
+
const re = /\{userId:([^}\s]+)\}/g
|
|
59
|
+
const seen = new Set<string>()
|
|
60
|
+
const out: string[] = []
|
|
61
|
+
let m: RegExpExecArray | null
|
|
62
|
+
while ((m = re.exec(text)) !== null) {
|
|
63
|
+
const id = m[1]
|
|
64
|
+
if (!seen.has(id)) {
|
|
65
|
+
seen.add(id)
|
|
66
|
+
out.push(id)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 본문에서 모든 `{key:value}` 인라인 마커 제거. LLM 입력 / textarea 편집 / 평문 표출 등.
|
|
74
|
+
* 토큰 자체 (#component, @user) 는 그대로 유지.
|
|
75
|
+
*/
|
|
76
|
+
export function stripMentionRefids(text: string | null | undefined): string {
|
|
77
|
+
if (!text) return ''
|
|
78
|
+
return text.replace(MENTION_MARKER_RE, '')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 토큰 매칭 정규식 빌더 — trigger 별로 build. */
|
|
82
|
+
function buildTokenRegex(trigger: string, markerKey: string): RegExp {
|
|
83
|
+
const word =
|
|
84
|
+
'\\w\\p{Script=Hangul}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Han}'
|
|
85
|
+
// (앞경계)(트리거)(토큰)(이미 같은 markerKey 의 마커가 붙어있는지 — skip 판정용)
|
|
86
|
+
return new RegExp(
|
|
87
|
+
`(^|[^${word}])(${escapeRegex(trigger)})([${word}-]+)(\\{${markerKey}:[^}\\s]+\\})?`,
|
|
88
|
+
'gu'
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeRegex(s: string): string {
|
|
93
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 일반화된 inline marker 주입 — trigger 와 markerKey 별로 picks 를 본문에 박음.
|
|
98
|
+
*
|
|
99
|
+
* injectInlineMarkers(text, '#', 'refid', [{ token, value: 35 }])
|
|
100
|
+
* injectInlineMarkers(text, '@', 'userId', [{ token, value: 'uuid' }])
|
|
101
|
+
*
|
|
102
|
+
* 룰:
|
|
103
|
+
* - 정확 token 일치 (popup 명시 선택된 것만 — 자유 입력 토큰은 안 건드림)
|
|
104
|
+
* - 동일 token 여러 번 등장하면 모두 marker 적용
|
|
105
|
+
* - 이미 같은 markerKey 의 marker 가 붙어있으면 그대로 유지 (덮어쓰기 X)
|
|
106
|
+
*/
|
|
107
|
+
export function injectInlineMarkers(
|
|
108
|
+
text: string,
|
|
109
|
+
trigger: string,
|
|
110
|
+
markerKey: string,
|
|
111
|
+
picks: ReadonlyArray<{ token: string; value: string | number }> | null | undefined
|
|
112
|
+
): string {
|
|
113
|
+
if (!text || !picks || picks.length === 0) return text ?? ''
|
|
114
|
+
const map = new Map<string, string | number>()
|
|
115
|
+
for (const p of picks) map.set(p.token, p.value)
|
|
116
|
+
const re = buildTokenRegex(trigger, markerKey)
|
|
117
|
+
return text.replace(re, (match, leading, trig, token, existingMarker) => {
|
|
118
|
+
if (existingMarker) return match
|
|
119
|
+
const value = map.get(token)
|
|
120
|
+
if (value === undefined) return match
|
|
121
|
+
return `${leading}${trig}${token}{${markerKey}:${value}}`
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `#component` 멘션 picks 를 `{refid:N}` 마커로 주입 (호환 유지).
|
|
127
|
+
*/
|
|
128
|
+
export function injectMentionRefids(
|
|
129
|
+
text: string,
|
|
130
|
+
picks: ReadonlyArray<MentionPick> | null | undefined
|
|
131
|
+
): string {
|
|
132
|
+
return injectInlineMarkers(
|
|
133
|
+
text,
|
|
134
|
+
'#',
|
|
135
|
+
'refid',
|
|
136
|
+
picks?.map(p => ({ token: p.token, value: p.refid }))
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `@user` 멘션 picks 를 `{userId:UUID}` 마커로 주입.
|
|
142
|
+
* picks: [{token: 사용자_표시명, value: 사용자_id}]
|
|
143
|
+
*/
|
|
144
|
+
export function injectMentionUserIds(
|
|
145
|
+
text: string,
|
|
146
|
+
picks: ReadonlyArray<{ token: string; userId: string }> | null | undefined
|
|
147
|
+
): string {
|
|
148
|
+
return injectInlineMarkers(
|
|
149
|
+
text,
|
|
150
|
+
'@',
|
|
151
|
+
'userId',
|
|
152
|
+
picks?.map(p => ({ token: p.token, value: p.userId }))
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* AI 응답 텍스트의 `#token` 들을 currentBoard 에 대해 resolve 해 inline marker 로 주입.
|
|
158
|
+
*
|
|
159
|
+
* - 매칭 룰: `findMentions` (= name/id/type/tag/class/text OR). 다중 일치 시 첫 결과.
|
|
160
|
+
* - 매칭 안 되면 marker 없이 통과 (예: AI 가 patch 로 새로 만든 컴포넌트는 input.currentBoard
|
|
161
|
+
* 에 없으므로 marker 못 박음 — 클라이언트 클릭 시 runtime fallback 으로 해석 가능).
|
|
162
|
+
* - 이미 marker 있으면 그대로.
|
|
163
|
+
*/
|
|
164
|
+
export function injectAiMentionRefids(
|
|
165
|
+
text: string,
|
|
166
|
+
currentBoard: BoardModel | null | undefined
|
|
167
|
+
): string {
|
|
168
|
+
if (!text) return ''
|
|
169
|
+
const matches = findMentions(text, currentBoard ?? null)
|
|
170
|
+
if (matches.length === 0) return text
|
|
171
|
+
|
|
172
|
+
// token → 첫 매칭 refid 맵
|
|
173
|
+
const tokenToRefid = new Map<string, number>()
|
|
174
|
+
for (const m of matches) {
|
|
175
|
+
if (tokenToRefid.has(m.token)) continue
|
|
176
|
+
if (m.matched.length === 0) continue
|
|
177
|
+
tokenToRefid.set(m.token, m.matched[0].refid)
|
|
178
|
+
}
|
|
179
|
+
if (tokenToRefid.size === 0) return text
|
|
180
|
+
|
|
181
|
+
// 사용자 picks 와 동일한 injection 룰로 주입.
|
|
182
|
+
const picks = Array.from(tokenToRefid.entries()).map(([token, refid]) => ({ token, refid }))
|
|
183
|
+
return injectMentionRefids(text, picks)
|
|
184
|
+
}
|