@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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mention-resolver.ts 단위 테스트.
|
|
3
|
+
*
|
|
4
|
+
* 핵심 정책 (things-factory 식별자 컨벤션):
|
|
5
|
+
* - refid 는 사용자 답변 / 본문 노출 금지 — 시스템 내부 핸들로만
|
|
6
|
+
* - # 는 와이드 셀렉터 — name / id / type OR 매칭
|
|
7
|
+
* - 본문 #token 은 LITERAL 보존, 매칭 결과는 끝의 mention map 블록으로
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
resolveMentions,
|
|
11
|
+
findMentions,
|
|
12
|
+
lookupComponents
|
|
13
|
+
} from './mention-resolver'
|
|
14
|
+
|
|
15
|
+
const make = (components: any[] = []) => ({ width: 1000, height: 600, components })
|
|
16
|
+
|
|
17
|
+
describe('lookupComponents — 와이드 검색 (name / id / type OR)', () => {
|
|
18
|
+
const components = [
|
|
19
|
+
{ refid: 1, type: 'rect', name: '경광등1', id: 'motor-1' },
|
|
20
|
+
{ refid: 2, type: 'rect', name: '경광등2', id: 'motor-2' },
|
|
21
|
+
{ refid: 3, type: 'circle', name: '센서', id: 'sensor-A' },
|
|
22
|
+
{ refid: 4, type: 'rect' } // name/id 없음
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
test('name 정확 일치', () => {
|
|
26
|
+
const r = lookupComponents('경광등1', components)
|
|
27
|
+
expect(r).toHaveLength(1)
|
|
28
|
+
expect(r[0].refid).toBe(1)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('id 정확 일치', () => {
|
|
32
|
+
const r = lookupComponents('motor-1', components)
|
|
33
|
+
expect(r).toHaveLength(1)
|
|
34
|
+
expect(r[0].refid).toBe(1)
|
|
35
|
+
expect(r[0].id).toBe('motor-1')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('type 정확 일치 — 같은 type 의 모든 컴포넌트', () => {
|
|
39
|
+
const r = lookupComponents('rect', components)
|
|
40
|
+
expect(r).toHaveLength(3)
|
|
41
|
+
expect(r.map(c => c.refid).sort()).toEqual([1, 2, 4])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('case-insensitive (영문)', () => {
|
|
45
|
+
expect(lookupComponents('MOTOR-1', components)).toHaveLength(1)
|
|
46
|
+
expect(lookupComponents('Rect', components)).toHaveLength(3)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('한글 case-insensitive 안전 (대소문자 개념 없지만 동작 확인)', () => {
|
|
50
|
+
expect(lookupComponents('경광등1', components)).toHaveLength(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('매칭 없음 → []', () => {
|
|
54
|
+
expect(lookupComponents('no-such', components)).toEqual([])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('빈 토큰 / 빈 components → []', () => {
|
|
58
|
+
expect(lookupComponents('', components)).toEqual([])
|
|
59
|
+
expect(lookupComponents('rect', [])).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('매칭된 컴포넌트의 핵심 식별자 반환 (refid + name + id + type)', () => {
|
|
63
|
+
const r = lookupComponents('motor-1', components)
|
|
64
|
+
expect(r[0]).toEqual({
|
|
65
|
+
refid: 1,
|
|
66
|
+
type: 'rect',
|
|
67
|
+
name: '경광등1',
|
|
68
|
+
id: 'motor-1'
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('동일 refid 두 기준 일치 시 중복 안 됨 (dedup)', () => {
|
|
73
|
+
// name 'rect' + type 'rect' — 인위적 케이스
|
|
74
|
+
const cs = [{ refid: 1, type: 'rect', name: 'rect' }]
|
|
75
|
+
const r = lookupComponents('rect', cs)
|
|
76
|
+
expect(r).toHaveLength(1) // 중복 X
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('resolveMentions — 본문 보존 + mention map 첨부', () => {
|
|
81
|
+
test('본문은 LITERAL 그대로, 매핑은 끝에 블록으로', () => {
|
|
82
|
+
const board = make([
|
|
83
|
+
{ refid: 35, type: 'rect', name: '경광등1', id: 'motor-1' }
|
|
84
|
+
])
|
|
85
|
+
const r = resolveMentions('#경광등1 빨갛게', board)
|
|
86
|
+
// 본문은 그대로 유지 — refid 본문 노출 0
|
|
87
|
+
expect(r).toContain('#경광등1 빨갛게')
|
|
88
|
+
expect(r).not.toMatch(/^경광등1\s*\(/m) // '(refid' 같은 inline 변환 없음
|
|
89
|
+
// mention map 블록 첨부
|
|
90
|
+
expect(r).toContain('--- mention map')
|
|
91
|
+
expect(r).toContain('#경광등1 →')
|
|
92
|
+
expect(r).toContain("name='경광등1'")
|
|
93
|
+
expect(r).toContain("id='motor-1'")
|
|
94
|
+
expect(r).toContain("type='rect'")
|
|
95
|
+
expect(r).toContain('refid=35')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('매칭 0 → 변경 없음 (본문 그대로)', () => {
|
|
99
|
+
const board = make([{ refid: 1, name: 'A' }])
|
|
100
|
+
expect(resolveMentions('#NoSuch 어쩌고', board)).toBe('#NoSuch 어쩌고')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('빈 components → 변경 없음', () => {
|
|
104
|
+
expect(resolveMentions('#anything', make([]))).toBe('#anything')
|
|
105
|
+
expect(resolveMentions('#anything', null)).toBe('#anything')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('빈 텍스트 안전', () => {
|
|
109
|
+
expect(resolveMentions('', make([{ refid: 1, name: 'A' }]))).toBe('')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('id 매칭 — 본문 #motor-1 그대로 + map 에 매핑', () => {
|
|
113
|
+
const board = make([{ refid: 5, type: 'rect', id: 'motor-1' }])
|
|
114
|
+
const r = resolveMentions('#motor-1 stop', board)
|
|
115
|
+
expect(r).toContain('#motor-1 stop')
|
|
116
|
+
expect(r).toContain('#motor-1 →')
|
|
117
|
+
expect(r).toContain("id='motor-1'")
|
|
118
|
+
expect(r).toContain('refid=5')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('type 매칭 — 같은 type 모든 컴포넌트가 map 에 나열', () => {
|
|
122
|
+
const board = make([
|
|
123
|
+
{ refid: 1, type: 'rect', name: 'A' },
|
|
124
|
+
{ refid: 2, type: 'rect', name: 'B' },
|
|
125
|
+
{ refid: 3, type: 'circle' }
|
|
126
|
+
])
|
|
127
|
+
const r = resolveMentions('#rect 모두', board)
|
|
128
|
+
expect(r).toContain('#rect 모두') // 본문 보존
|
|
129
|
+
expect(r).toContain('#rect →')
|
|
130
|
+
expect(r).toContain('refid=1')
|
|
131
|
+
expect(r).toContain('refid=2')
|
|
132
|
+
expect(r).not.toContain('refid=3') // circle 제외
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('다중 #token — 모두 map 에 등록, 본문 모두 보존', () => {
|
|
136
|
+
const board = make([
|
|
137
|
+
{ refid: 1, name: 'A' },
|
|
138
|
+
{ refid: 2, name: 'B' }
|
|
139
|
+
])
|
|
140
|
+
const r = resolveMentions('#A 와 #B', board)
|
|
141
|
+
expect(r).toContain('#A 와 #B')
|
|
142
|
+
expect(r).toContain('#A →')
|
|
143
|
+
expect(r).toContain('#B →')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('일부 매칭 일부 실패 — 매칭 성공만 map 에', () => {
|
|
147
|
+
const board = make([{ refid: 1, name: 'A' }])
|
|
148
|
+
const r = resolveMentions('#A and #X', board)
|
|
149
|
+
expect(r).toContain('#A and #X') // 본문 모두 보존
|
|
150
|
+
expect(r).toContain('#A →')
|
|
151
|
+
expect(r).not.toContain('#X →') // X 매칭 실패
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('refid 가 본문에 등장하는 다른 사용자 텍스트는 영향 없음 (token 외 #refid 표기)', () => {
|
|
155
|
+
// 사용자가 어쩌다 'refid' 라고 본문에 쓴 경우는 우리가 변환 안 함
|
|
156
|
+
const board = make([{ refid: 1, name: 'A' }])
|
|
157
|
+
const r = resolveMentions('#A 의 refid 가 뭐야?', board)
|
|
158
|
+
expect(r).toContain('#A 의 refid 가 뭐야?')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('findMentions — 매칭 메타 직접 노출 (UI / 디버그)', () => {
|
|
163
|
+
test('성공 + 실패 모두 반환, 위치 포함', () => {
|
|
164
|
+
const board = make([
|
|
165
|
+
{ refid: 1, name: 'A' },
|
|
166
|
+
{ refid: 2, name: 'B' }
|
|
167
|
+
])
|
|
168
|
+
const r = findMentions('#A #X #B', board)
|
|
169
|
+
expect(r).toHaveLength(3)
|
|
170
|
+
expect(r[0].token).toBe('A')
|
|
171
|
+
expect(r[0].matched).toHaveLength(1)
|
|
172
|
+
expect(r[0].matched[0].refid).toBe(1)
|
|
173
|
+
expect(r[1].token).toBe('X')
|
|
174
|
+
expect(r[1].matched).toEqual([])
|
|
175
|
+
expect(r[2].token).toBe('B')
|
|
176
|
+
expect(r[2].matched[0].refid).toBe(2)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('빈 텍스트 → []', () => {
|
|
180
|
+
expect(findMentions('', make([]))).toEqual([])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('start/end 위치 정확', () => {
|
|
184
|
+
const board = make([{ refid: 1, name: 'A' }])
|
|
185
|
+
const r = findMentions('hello #A world', board)
|
|
186
|
+
expect(r[0].start).toBe(6)
|
|
187
|
+
expect(r[0].end).toBe(8)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('picks (popup 명시 선택) — 결정적 매칭', () => {
|
|
192
|
+
test('pick 의 token + refid → 그 컴포넌트 직접 매칭 (fallback 안 거침)', () => {
|
|
193
|
+
const board = make([
|
|
194
|
+
{ refid: 35, name: '경광등1', type: 'rect' },
|
|
195
|
+
{ refid: 42, name: '경광등1', type: 'rect' } // 동명이인
|
|
196
|
+
])
|
|
197
|
+
// picks 없으면 둘 다 매칭 (다중)
|
|
198
|
+
const noPicks = lookupComponents('경광등1', board.components)
|
|
199
|
+
expect(noPicks).toHaveLength(2)
|
|
200
|
+
// picks 있으면 그 refid 만
|
|
201
|
+
const withPicks = lookupComponents('경광등1', board.components, [
|
|
202
|
+
{ token: '경광등1', refid: 35 }
|
|
203
|
+
])
|
|
204
|
+
expect(withPicks).toHaveLength(1)
|
|
205
|
+
expect(withPicks[0].refid).toBe(35)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('pick 의 refid 가 stale (보드에 없음) → fallback 으로', () => {
|
|
209
|
+
const board = make([{ refid: 35, name: '경광등1' }])
|
|
210
|
+
const r = lookupComponents('경광등1', board.components, [
|
|
211
|
+
{ token: '경광등1', refid: 999 } // stale
|
|
212
|
+
])
|
|
213
|
+
expect(r).toHaveLength(1)
|
|
214
|
+
expect(r[0].refid).toBe(35) // fallback 으로 name 매칭
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('pick token 이 텍스트 token 과 다르면 무시 (다른 token 의 pick)', () => {
|
|
218
|
+
const board = make([
|
|
219
|
+
{ refid: 1, name: 'A' },
|
|
220
|
+
{ refid: 2, name: 'B' }
|
|
221
|
+
])
|
|
222
|
+
const r = lookupComponents('A', board.components, [
|
|
223
|
+
{ token: 'B', refid: 2 } // A 가 아니라 B 의 pick
|
|
224
|
+
])
|
|
225
|
+
expect(r).toHaveLength(1)
|
|
226
|
+
expect(r[0].refid).toBe(1) // A name 매칭
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('resolveMentions options.picks 통과', () => {
|
|
230
|
+
const board = make([
|
|
231
|
+
{ refid: 35, name: '경광등1' },
|
|
232
|
+
{ refid: 42, name: '경광등1' }
|
|
233
|
+
])
|
|
234
|
+
const r = resolveMentions('#경광등1 빨갛게', board, {
|
|
235
|
+
picks: [{ token: '경광등1', refid: 42 }]
|
|
236
|
+
})
|
|
237
|
+
expect(r).toContain('refid=42')
|
|
238
|
+
expect(r).not.toContain('refid=35') // pick 결정적 — 35 안 옴
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('mention map 에 position 정보 포함 (AI 좌표 인식 보조)', () => {
|
|
242
|
+
const board = make([
|
|
243
|
+
{ refid: 1, name: 'A', type: 'rect', left: 100, top: 200, width: 80, height: 40 }
|
|
244
|
+
])
|
|
245
|
+
const r = resolveMentions('#A', board)
|
|
246
|
+
// 중심 좌표 (140, 220)
|
|
247
|
+
expect(r).toContain('pos=(140,220)')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('refid 정책 회귀 방지', () => {
|
|
252
|
+
test('"#refidNN" 같은 직접 refid 참조는 더 이상 작동 안 함 (refid 사용자 노출 금지)', () => {
|
|
253
|
+
const board = make([{ refid: 35, name: '경광등' }])
|
|
254
|
+
// 과거에는 #refid35 가 직접 참조됐지만 이제는 일반 token 으로만 처리
|
|
255
|
+
// 'refid35' 라는 name/id/type 가 없으면 매칭 안 됨
|
|
256
|
+
const r = resolveMentions('#refid35 빨갛게', board)
|
|
257
|
+
expect(r).toBe('#refid35 빨갛게') // 매칭 0 → 변경 없음
|
|
258
|
+
expect(r).not.toContain('mention map')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('mention map 블록 안에서만 refid 등장 (본문에는 없음)', () => {
|
|
262
|
+
const board = make([{ refid: 35, name: '경광등1' }])
|
|
263
|
+
const r = resolveMentions('#경광등1 빨갛게', board)
|
|
264
|
+
// 본문 영역 (mention map 블록 전) 에는 refid 단어가 없어야
|
|
265
|
+
const mapStart = r.indexOf('--- mention map')
|
|
266
|
+
const bodyPart = r.slice(0, mapStart)
|
|
267
|
+
expect(bodyPart).not.toMatch(/refid/i)
|
|
268
|
+
// map 블록 안에는 refid 가 있다 (AI 가 tool 호출에 사용)
|
|
269
|
+
const mapPart = r.slice(mapStart)
|
|
270
|
+
expect(mapPart).toMatch(/refid=35/)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `#token` 멘션 해석 (things-factory 식별자 컨벤션 준수).
|
|
3
|
+
*
|
|
4
|
+
* things-factory 정책:
|
|
5
|
+
* - `#` 은 사용자가 컴포넌트를 가리키는 SELECTOR. things-scene Finder 의
|
|
6
|
+
* CSS 셀렉터 컨셉과 같은 맥락 — 와이드 검색 (name / id / type 셋 다 OR).
|
|
7
|
+
* - **refid 는 시스템 내부 핸들** — 사용자에게 노출 금지. 사용자가 본 적도
|
|
8
|
+
* 없는 숫자라 `(refid 35)` 같은 표기는 혼란만 가중.
|
|
9
|
+
* - id 는 데이터 바인딩 식별자 (human-readable). unique 제약 없음.
|
|
10
|
+
* - name 은 사용자 라벨. 가장 친숙.
|
|
11
|
+
*
|
|
12
|
+
* Resolver 정책:
|
|
13
|
+
* - 본문의 `#token` 은 LITERAL 보존 (refid 본문 노출 0)
|
|
14
|
+
* - 매칭 결과는 본문 끝에 구조화된 "mention map" 블록으로 첨부
|
|
15
|
+
* - AI 는 시스템 프롬프트의 안내에 따라 mention map 의 refid 를 tool
|
|
16
|
+
* 호출 인자로만 사용. 사용자 답변 텍스트에서는 name/id 로 지칭.
|
|
17
|
+
*
|
|
18
|
+
* Token 매칭 (모두 시도, OR):
|
|
19
|
+
* - name 정확 일치 (case-insensitive)
|
|
20
|
+
* - id 정확 일치 (case-insensitive)
|
|
21
|
+
* - type 정확 일치 (case-insensitive) — `#rect` 같이 모든 type 가리키기
|
|
22
|
+
*
|
|
23
|
+
* 부분 / fuzzy 매칭 안 함 — popup 측에서 사용자가 정확한 식별자 선택.
|
|
24
|
+
*
|
|
25
|
+
* Token 정규식: /#([^\s#]+)/g (한글/영문/하이픈/언더스코어/숫자).
|
|
26
|
+
*/
|
|
27
|
+
import type { BoardModel } from '@things-factory/board-import'
|
|
28
|
+
|
|
29
|
+
const MENTION_PATTERN = /#([^\s#]+)/g
|
|
30
|
+
|
|
31
|
+
/** 매칭된 컴포넌트의 핵심 정보 — refid 는 시스템 핸들, 나머지는 사용자 친화적 식별.
|
|
32
|
+
* things-scene F-Finder 와 동일한 검색 필드 (id/name/type/tag/class/text)
|
|
33
|
+
* + position (AI 가 식별 + 조작 좌표 판단에 활용). */
|
|
34
|
+
export interface MentionedComponent {
|
|
35
|
+
refid: number
|
|
36
|
+
name?: string
|
|
37
|
+
id?: string
|
|
38
|
+
type: string
|
|
39
|
+
tag?: string
|
|
40
|
+
class?: string
|
|
41
|
+
text?: string
|
|
42
|
+
/** 중심 좌표 (rect: left+w/2, top+h/2 / circle: cx, cy) — 정수 반올림 */
|
|
43
|
+
position?: { x: number; y: number }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** popup 에서 사용자가 명시 선택한 컴포넌트 — server 가 정확한 매칭에 사용. */
|
|
47
|
+
export interface MentionPick {
|
|
48
|
+
/** 사용자 텍스트의 #token 부분 (예: '경광등1') */
|
|
49
|
+
token: string
|
|
50
|
+
/** popup 에서 선택된 정확한 refid */
|
|
51
|
+
refid: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ResolveMentionsOptions {
|
|
55
|
+
/** popup 에서 명시 선택된 mentions — 해당 token 은 fallback 매칭 안 거치고 정확한 refid 로 즉시 해석 */
|
|
56
|
+
picks?: MentionPick[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MentionMatch {
|
|
60
|
+
/** 사용자가 입력한 #token 의 token 부분 */
|
|
61
|
+
token: string
|
|
62
|
+
/** 토큰 위치 (start: '#' 위치, end: 마지막 글자 다음) */
|
|
63
|
+
start: number
|
|
64
|
+
end: number
|
|
65
|
+
/** name/id/type 매칭으로 발견한 컴포넌트들 */
|
|
66
|
+
matched: MentionedComponent[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 텍스트의 모든 #token 을 보드에서 매칭. UI / 디버그 / 테스트용.
|
|
71
|
+
*/
|
|
72
|
+
export function findMentions(
|
|
73
|
+
text: string,
|
|
74
|
+
currentBoard: BoardModel | null | undefined,
|
|
75
|
+
options: ResolveMentionsOptions = {}
|
|
76
|
+
): MentionMatch[] {
|
|
77
|
+
if (!text || typeof text !== 'string') return []
|
|
78
|
+
const components = currentBoard?.components ?? []
|
|
79
|
+
return collectMatches(text, components, options.picks)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Token 으로 컴포넌트 매칭 — name/id/type/tag/class/text OR (things-scene F-Finder
|
|
85
|
+
* 동일 필드 셋). 여러 기준에서 동시 일치하더라도 같은 refid 는 한 번만 반환 (dedup).
|
|
86
|
+
*
|
|
87
|
+
* picks 에 해당 token 이 있으면 fallback 안 거치고 그 refid 로 직접 매칭 (popup 에서
|
|
88
|
+
* 사용자가 명시 선택한 결정적 매핑). 그 refid 가 보드에 없으면 fallback 으로.
|
|
89
|
+
*/
|
|
90
|
+
export function lookupComponents(
|
|
91
|
+
token: string,
|
|
92
|
+
components: readonly any[],
|
|
93
|
+
picks?: readonly MentionPick[]
|
|
94
|
+
): MentionedComponent[] {
|
|
95
|
+
if (!token || components.length === 0) return []
|
|
96
|
+
|
|
97
|
+
// picks 우선 — popup 으로 명시 선택된 token 은 그 refid 로 직접 매칭
|
|
98
|
+
if (picks) {
|
|
99
|
+
const pick = picks.find(p => p.token === token)
|
|
100
|
+
if (pick) {
|
|
101
|
+
const c = components.find(
|
|
102
|
+
(x: any) => x && typeof x.refid === 'number' && x.refid === pick.refid
|
|
103
|
+
)
|
|
104
|
+
if (c) {
|
|
105
|
+
const fields = readSearchFields(c)
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
refid: pick.refid,
|
|
109
|
+
name: fields.name,
|
|
110
|
+
id: fields.id,
|
|
111
|
+
type: fields.type,
|
|
112
|
+
tag: fields.tag,
|
|
113
|
+
class: fields.cls,
|
|
114
|
+
text: fields.text,
|
|
115
|
+
position: extractPosition(c)
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
// pick 이 stale (refid 가 더 이상 보드에 없음) — fallback 으로 진행
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lower = token.toLowerCase()
|
|
124
|
+
const out: MentionedComponent[] = []
|
|
125
|
+
const seen = new Set<number>()
|
|
126
|
+
for (const c of components) {
|
|
127
|
+
if (!c || typeof (c as any).refid !== 'number') continue
|
|
128
|
+
const refid = (c as any).refid
|
|
129
|
+
if (seen.has(refid)) continue
|
|
130
|
+
const fields = readSearchFields(c)
|
|
131
|
+
if (
|
|
132
|
+
(fields.name && fields.name.toLowerCase() === lower) ||
|
|
133
|
+
(fields.id && fields.id.toLowerCase() === lower) ||
|
|
134
|
+
fields.type.toLowerCase() === lower ||
|
|
135
|
+
(fields.tag && fields.tag.toLowerCase() === lower) ||
|
|
136
|
+
(fields.cls && fields.cls.toLowerCase() === lower) ||
|
|
137
|
+
(fields.text && fields.text.toLowerCase() === lower)
|
|
138
|
+
) {
|
|
139
|
+
seen.add(refid)
|
|
140
|
+
out.push({
|
|
141
|
+
refid,
|
|
142
|
+
name: fields.name,
|
|
143
|
+
id: fields.id,
|
|
144
|
+
type: fields.type,
|
|
145
|
+
tag: fields.tag,
|
|
146
|
+
class: fields.cls,
|
|
147
|
+
text: fields.text,
|
|
148
|
+
position: extractPosition(c)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 컴포넌트 중심 좌표 — rect: left+w/2 / circle: cx,cy. */
|
|
156
|
+
function extractPosition(c: any): { x: number; y: number } | undefined {
|
|
157
|
+
if (typeof c.cx === 'number' && typeof c.cy === 'number') {
|
|
158
|
+
return { x: Math.round(c.cx), y: Math.round(c.cy) }
|
|
159
|
+
}
|
|
160
|
+
if (typeof c.left === 'number' && typeof c.top === 'number') {
|
|
161
|
+
const w = typeof c.width === 'number' ? c.width : 0
|
|
162
|
+
const h = typeof c.height === 'number' ? c.height : 0
|
|
163
|
+
return { x: Math.round(c.left + w / 2), y: Math.round(c.top + h / 2) }
|
|
164
|
+
}
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readSearchFields(c: any): {
|
|
169
|
+
name?: string
|
|
170
|
+
id?: string
|
|
171
|
+
type: string
|
|
172
|
+
tag?: string
|
|
173
|
+
cls?: string
|
|
174
|
+
text?: string
|
|
175
|
+
} {
|
|
176
|
+
const str = (v: any): string | undefined =>
|
|
177
|
+
typeof v === 'string' && v.length > 0 ? v : undefined
|
|
178
|
+
return {
|
|
179
|
+
name: str(c.name),
|
|
180
|
+
id: str(c.id),
|
|
181
|
+
type: typeof c.type === 'string' ? c.type : 'unknown',
|
|
182
|
+
tag: str(c.tag),
|
|
183
|
+
cls: str(c.class),
|
|
184
|
+
text: str(c.text)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 텍스트의 #token 을 LLM 친화 형태로 변환.
|
|
190
|
+
*
|
|
191
|
+
* 본문은 그대로 + 매칭 성공한 토큰의 mention map 을 끝에 첨부:
|
|
192
|
+
*
|
|
193
|
+
* "[원본 본문]
|
|
194
|
+
*
|
|
195
|
+
* --- mention map (자동 첨부 — 사용자가 # 로 지칭한 컴포넌트) ---
|
|
196
|
+
* #경광등1 →
|
|
197
|
+
* { name='경광등1', id='motor-1', type='rect', refid=35 }
|
|
198
|
+
* #펌프 →
|
|
199
|
+
* { name='펌프A', id='pump-1', type='rect', refid=42 }
|
|
200
|
+
* { name='펌프B', id='pump-2', type='rect', refid=51 }"
|
|
201
|
+
*
|
|
202
|
+
* AI 는 system prompt 안내에 따라 refid 를 tool 호출 인자로만 사용. 답변
|
|
203
|
+
* 텍스트에서는 refid 직접 언급 금지.
|
|
204
|
+
*
|
|
205
|
+
* 매칭 0 → 변경 없음 (본문 그대로 반환).
|
|
206
|
+
*/
|
|
207
|
+
export function resolveMentions(
|
|
208
|
+
text: string,
|
|
209
|
+
currentBoard: BoardModel | null | undefined,
|
|
210
|
+
options: ResolveMentionsOptions = {}
|
|
211
|
+
): string {
|
|
212
|
+
if (!text || typeof text !== 'string') return text
|
|
213
|
+
const components = currentBoard?.components ?? []
|
|
214
|
+
if (components.length === 0) return text
|
|
215
|
+
|
|
216
|
+
const matches = collectMatches(text, components, options.picks)
|
|
217
|
+
const resolved = matches.filter(m => m.matched.length > 0)
|
|
218
|
+
if (resolved.length === 0) return text
|
|
219
|
+
|
|
220
|
+
const block: string[] = [
|
|
221
|
+
'',
|
|
222
|
+
'--- mention map (자동 첨부 — 사용자가 # 로 지칭한 컴포넌트) ---'
|
|
223
|
+
]
|
|
224
|
+
for (const m of resolved) {
|
|
225
|
+
block.push(`#${m.token} →`)
|
|
226
|
+
for (const c of m.matched) {
|
|
227
|
+
block.push(` ${formatComponentRef(c)}`)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return text + '\n' + block.join('\n')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function collectMatches(
|
|
234
|
+
text: string,
|
|
235
|
+
components: readonly any[],
|
|
236
|
+
picks?: readonly MentionPick[]
|
|
237
|
+
): MentionMatch[] {
|
|
238
|
+
const out: MentionMatch[] = []
|
|
239
|
+
const pattern = new RegExp(MENTION_PATTERN.source, 'g')
|
|
240
|
+
let m: RegExpExecArray | null
|
|
241
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
242
|
+
const token = m[1]
|
|
243
|
+
out.push({
|
|
244
|
+
token,
|
|
245
|
+
start: m.index,
|
|
246
|
+
end: m.index + m[0].length,
|
|
247
|
+
matched: lookupComponents(token, components, picks)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
return out
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function formatComponentRef(c: MentionedComponent): string {
|
|
254
|
+
const parts: string[] = []
|
|
255
|
+
if (c.name) parts.push(`name='${c.name}'`)
|
|
256
|
+
if (c.id) parts.push(`id='${c.id}'`)
|
|
257
|
+
parts.push(`type='${c.type}'`)
|
|
258
|
+
if (c.tag) parts.push(`tag='${c.tag}'`)
|
|
259
|
+
if (c.class) parts.push(`class='${c.class}'`)
|
|
260
|
+
if (c.text) parts.push(`text='${c.text}'`)
|
|
261
|
+
if (c.position) parts.push(`pos=(${c.position.x},${c.position.y})`)
|
|
262
|
+
parts.push(`refid=${c.refid}`)
|
|
263
|
+
return `{ ${parts.join(', ')} }`
|
|
264
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* board-patch 브로드캐스트 — 순수 로직 (토픽/페이로드/필터). 데코레이터·pubsub import 없음
|
|
3
|
+
* → 단위테스트 가능. 데코레이터 resolver(board-patch-subscription.ts)와 pubsub 의존
|
|
4
|
+
* publishBoardPatch 는 별도.
|
|
5
|
+
*/
|
|
6
|
+
import type { PatchEntry } from './patch-entry.js' // type-only — 런타임 import 아님
|
|
7
|
+
|
|
8
|
+
/** board-patch 브로드캐스트 토픽. */
|
|
9
|
+
export const BOARD_PATCH_TOPIC = 'board-patch'
|
|
10
|
+
|
|
11
|
+
/** 방송 페이로드 — 구독 필터(domain+board)용 메타 + PatchEntry 본체. */
|
|
12
|
+
export interface BoardPatchPayload {
|
|
13
|
+
domainId: string
|
|
14
|
+
boardId?: string
|
|
15
|
+
patch: PatchEntry
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 같은 domain + board 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).
|
|
20
|
+
*/
|
|
21
|
+
export function boardPatchFilter(domainId: string, boardId: string) {
|
|
22
|
+
return (payload: BoardPatchPayload): boolean =>
|
|
23
|
+
!!payload && payload.domainId === domainId && payload.boardId === boardId
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-patch 브로드캐스트 — 단위 테스트 (순수 로직).
|
|
3
|
+
* - boardPatchFilter: domain+board 격리(보안 핵심) — 일치만 통과, 불일치/널/board없음 차단.
|
|
4
|
+
* (publishBoardPatch / 구독 resolver 는 pubsub·graphql-ws 통합 영역 — 별도 검증.)
|
|
5
|
+
*/
|
|
6
|
+
import { boardPatchFilter, BOARD_PATCH_TOPIC } from './board-patch-broadcast'
|
|
7
|
+
import type { BoardPatchPayload } from './board-patch-broadcast'
|
|
8
|
+
|
|
9
|
+
const payload = (domainId: string, boardId?: string): BoardPatchPayload => ({
|
|
10
|
+
domainId,
|
|
11
|
+
boardId,
|
|
12
|
+
patch: { id: 'p1' } as any
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('boardPatchFilter (domain+board 격리)', () => {
|
|
16
|
+
it('domain+board 모두 일치 → 통과', () => {
|
|
17
|
+
expect(boardPatchFilter('d1', 'b1')(payload('d1', 'b1'))).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('domain 불일치 → 차단 (도메인 격리)', () => {
|
|
21
|
+
expect(boardPatchFilter('d1', 'b1')(payload('d2', 'b1'))).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('board 불일치 → 차단', () => {
|
|
25
|
+
expect(boardPatchFilter('d1', 'b1')(payload('d1', 'b2'))).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('boardId 없는 페이로드 → 차단', () => {
|
|
29
|
+
expect(boardPatchFilter('d1', 'b1')(payload('d1', undefined))).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('null/undefined 페이로드 안전 → 차단', () => {
|
|
33
|
+
expect(boardPatchFilter('d1', 'b1')(null as any)).toBe(false)
|
|
34
|
+
expect(boardPatchFilter('d1', 'b1')(undefined as any)).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('토픽 상수', () => {
|
|
38
|
+
expect(BOARD_PATCH_TOPIC).toBe('board-patch')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* BoardPatchSubscription (Phase Board-AI C1 — op-브로드캐스트 spine).
|
|
3
|
+
*
|
|
4
|
+
* PatchEntry(AI 또는 user-direct 편집) 가 생성될 때마다 같은 board 구독자에게 방송 →
|
|
5
|
+
* 실시간 공동편집의 척추. 클라이언트(C2)가 구독해 원격 patch 를 applyBoardEditPatch 로 적용.
|
|
6
|
+
*
|
|
7
|
+
* 전송/패턴은 기존 board-service/board-subscription.ts 복제 (PubSub + graphql-ws, domain 격리).
|
|
8
|
+
* 순수 로직(토픽/필터)은 board-patch-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.
|
|
9
|
+
*/
|
|
10
|
+
import { Resolver, Subscription, Root, Arg } from 'type-graphql'
|
|
11
|
+
import { filter, pipe } from 'graphql-yoga'
|
|
12
|
+
import { pubsub } from '@things-factory/shell'
|
|
13
|
+
import { PatchEntry } from './patch-entry.js'
|
|
14
|
+
import { BOARD_PATCH_TOPIC, boardPatchFilter, type BoardPatchPayload } from './board-patch-broadcast.js'
|
|
15
|
+
import { assertDomainSubscribeAllowed } from '../subscribe-auth.js'
|
|
16
|
+
|
|
17
|
+
export { BOARD_PATCH_TOPIC, boardPatchFilter } from './board-patch-broadcast.js'
|
|
18
|
+
export type { BoardPatchPayload } from './board-patch-broadcast.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PatchEntry 생성 직후 호출 — 같은 board 구독자에게 방송.
|
|
22
|
+
* boardId 없으면(세션 미연결) skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.
|
|
23
|
+
*/
|
|
24
|
+
export function publishBoardPatch(payload: BoardPatchPayload): void {
|
|
25
|
+
if (!payload.boardId) return
|
|
26
|
+
try {
|
|
27
|
+
pubsub.publish(BOARD_PATCH_TOPIC, payload)
|
|
28
|
+
} catch {
|
|
29
|
+
/* 방송 생략 — 영속은 이미 끝남 */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Resolver(PatchEntry)
|
|
34
|
+
export class BoardPatchSubscription {
|
|
35
|
+
@Subscription({
|
|
36
|
+
description:
|
|
37
|
+
'Subscribes to board edit patches (AI or user-direct) for real-time collaboration on a board.',
|
|
38
|
+
subscribe: async ({ args, context }) => {
|
|
39
|
+
const { domain, user } = context.state
|
|
40
|
+
const { boardId } = args
|
|
41
|
+
|
|
42
|
+
await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))
|
|
43
|
+
|
|
44
|
+
return pipe(pubsub.subscribe(BOARD_PATCH_TOPIC), filter(boardPatchFilter(domain.id, boardId)))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
boardPatchActivity(@Root() payload: BoardPatchPayload, @Arg('boardId') boardId: string): PatchEntry {
|
|
48
|
+
return payload.patch
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PatchEntry } from './patch-entry.js'
|
|
2
|
+
|
|
3
|
+
export { PatchEntry }
|
|
4
|
+
export {
|
|
5
|
+
BoardPatchSubscription,
|
|
6
|
+
publishBoardPatch,
|
|
7
|
+
boardPatchFilter,
|
|
8
|
+
BOARD_PATCH_TOPIC,
|
|
9
|
+
type BoardPatchPayload
|
|
10
|
+
} from './board-patch-subscription.js'
|
|
11
|
+
|
|
12
|
+
export const entities = [PatchEntry]
|