@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.
Files changed (329) hide show
  1. package/PLAN-styling-tools.md +1253 -0
  2. package/client/components/board-ai-chat.test.ts +149 -0
  3. package/client/components/board-ai-chat.ts +2574 -0
  4. package/client/components/chat-echo-dedup.test.ts +116 -0
  5. package/client/components/chat-echo-dedup.ts +89 -0
  6. package/client/components/chat-input-builder.ts +47 -0
  7. package/client/components/default-catalog-entries.test.ts +59 -0
  8. package/client/components/default-catalog-entries.ts +100 -0
  9. package/client/components/default-slash-templates.test.ts +55 -0
  10. package/client/components/default-slash-templates.ts +129 -0
  11. package/client/components/markdown.test.ts +518 -0
  12. package/client/components/markdown.ts +508 -0
  13. package/client/components/mention-popup-helpers.ts +433 -0
  14. package/client/components/mention-popup.test.ts +588 -0
  15. package/client/components/mention-popup.ts +238 -0
  16. package/client/index.ts +11 -0
  17. package/client/tsconfig.json +13 -0
  18. package/client/utils/board-edit-patch.ts +374 -0
  19. package/config/config.development.js +43 -0
  20. package/config/config.production.js +15 -0
  21. package/dist-client/client/components/board-ai-chat.d.ts +277 -0
  22. package/dist-client/client/components/board-ai-chat.js +2444 -0
  23. package/dist-client/client/components/board-ai-chat.js.map +1 -0
  24. package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
  25. package/dist-client/client/components/board-ai-chat.test.js +138 -0
  26. package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
  27. package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
  28. package/dist-client/client/components/chat-echo-dedup.js +58 -0
  29. package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
  30. package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
  31. package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
  32. package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
  33. package/dist-client/client/components/chat-input-builder.d.ts +39 -0
  34. package/dist-client/client/components/chat-input-builder.js +26 -0
  35. package/dist-client/client/components/chat-input-builder.js.map +1 -0
  36. package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
  37. package/dist-client/client/components/default-catalog-entries.js +86 -0
  38. package/dist-client/client/components/default-catalog-entries.js.map +1 -0
  39. package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
  40. package/dist-client/client/components/default-catalog-entries.test.js +52 -0
  41. package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
  42. package/dist-client/client/components/default-slash-templates.d.ts +17 -0
  43. package/dist-client/client/components/default-slash-templates.js +113 -0
  44. package/dist-client/client/components/default-slash-templates.js.map +1 -0
  45. package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
  46. package/dist-client/client/components/default-slash-templates.test.js +51 -0
  47. package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
  48. package/dist-client/client/components/markdown.d.ts +60 -0
  49. package/dist-client/client/components/markdown.js +422 -0
  50. package/dist-client/client/components/markdown.js.map +1 -0
  51. package/dist-client/client/components/markdown.test.d.ts +1 -0
  52. package/dist-client/client/components/markdown.test.js +418 -0
  53. package/dist-client/client/components/markdown.test.js.map +1 -0
  54. package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
  55. package/dist-client/client/components/mention-popup-helpers.js +330 -0
  56. package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
  57. package/dist-client/client/components/mention-popup.d.ts +35 -0
  58. package/dist-client/client/components/mention-popup.js +223 -0
  59. package/dist-client/client/components/mention-popup.js.map +1 -0
  60. package/dist-client/client/components/mention-popup.test.d.ts +1 -0
  61. package/dist-client/client/components/mention-popup.test.js +474 -0
  62. package/dist-client/client/components/mention-popup.test.js.map +1 -0
  63. package/dist-client/client/index.d.ts +11 -0
  64. package/dist-client/client/index.js +12 -0
  65. package/dist-client/client/index.js.map +1 -0
  66. package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
  67. package/dist-client/client/utils/board-edit-patch.js +284 -0
  68. package/dist-client/client/utils/board-edit-patch.js.map +1 -0
  69. package/dist-client/server/service/agentic-loop.d.ts +87 -0
  70. package/dist-client/server/service/agentic-loop.js +276 -0
  71. package/dist-client/server/service/agentic-loop.js.map +1 -0
  72. package/dist-client/server/service/assistant.d.ts +84 -0
  73. package/dist-client/server/service/assistant.js +2220 -0
  74. package/dist-client/server/service/assistant.js.map +1 -0
  75. package/dist-client/server/service/catalog-resolver.d.ts +18 -0
  76. package/dist-client/server/service/catalog-resolver.js +93 -0
  77. package/dist-client/server/service/catalog-resolver.js.map +1 -0
  78. package/dist-client/server/service/component-tree.d.ts +68 -0
  79. package/dist-client/server/service/component-tree.js +68 -0
  80. package/dist-client/server/service/component-tree.js.map +1 -0
  81. package/dist-client/server/service/mention-resolver.d.ts +96 -0
  82. package/dist-client/server/service/mention-resolver.js +172 -0
  83. package/dist-client/server/service/mention-resolver.js.map +1 -0
  84. package/dist-client/server/service/prefab-registry.d.ts +73 -0
  85. package/dist-client/server/service/prefab-registry.js +252 -0
  86. package/dist-client/server/service/prefab-registry.js.map +1 -0
  87. package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
  88. package/dist-client/server/service/styling/clear-tools.js +50 -0
  89. package/dist-client/server/service/styling/clear-tools.js.map +1 -0
  90. package/dist-client/server/service/styling/color-utils.d.ts +43 -0
  91. package/dist-client/server/service/styling/color-utils.js +205 -0
  92. package/dist-client/server/service/styling/color-utils.js.map +1 -0
  93. package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
  94. package/dist-client/server/service/styling/copy-tools.js +78 -0
  95. package/dist-client/server/service/styling/copy-tools.js.map +1 -0
  96. package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
  97. package/dist-client/server/service/styling/effect-tools.js +85 -0
  98. package/dist-client/server/service/styling/effect-tools.js.map +1 -0
  99. package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
  100. package/dist-client/server/service/styling/fill-tools.js +226 -0
  101. package/dist-client/server/service/styling/fill-tools.js.map +1 -0
  102. package/dist-client/server/service/styling/material-tools.d.ts +32 -0
  103. package/dist-client/server/service/styling/material-tools.js +88 -0
  104. package/dist-client/server/service/styling/material-tools.js.map +1 -0
  105. package/dist-client/server/service/styling/read-tools.d.ts +34 -0
  106. package/dist-client/server/service/styling/read-tools.js +42 -0
  107. package/dist-client/server/service/styling/read-tools.js.map +1 -0
  108. package/dist-client/server/service/styling/registry.d.ts +91 -0
  109. package/dist-client/server/service/styling/registry.js +345 -0
  110. package/dist-client/server/service/styling/registry.js.map +1 -0
  111. package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
  112. package/dist-client/server/service/styling/stroke-tools.js +94 -0
  113. package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
  114. package/dist-client/server/service/styling/text-tools.d.ts +32 -0
  115. package/dist-client/server/service/styling/text-tools.js +101 -0
  116. package/dist-client/server/service/styling/text-tools.js.map +1 -0
  117. package/dist-client/server/service/types.d.ts +265 -0
  118. package/dist-client/server/service/types.js +2 -0
  119. package/dist-client/server/service/types.js.map +1 -0
  120. package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
  121. package/dist-client/server/service/validation/board-model-schema.js +402 -0
  122. package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
  123. package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
  124. package/dist-client/server/service/validation/tool-validation.js +193 -0
  125. package/dist-client/server/service/validation/tool-validation.js.map +1 -0
  126. package/dist-client/tsconfig.tsbuildinfo +1 -0
  127. package/dist-server/index.d.ts +23 -0
  128. package/dist-server/index.js +27 -0
  129. package/dist-server/index.js.map +1 -0
  130. package/dist-server/service/agentic-loop.d.ts +87 -0
  131. package/dist-server/service/agentic-loop.js +280 -0
  132. package/dist-server/service/agentic-loop.js.map +1 -0
  133. package/dist-server/service/apply-patch.d.ts +47 -0
  134. package/dist-server/service/apply-patch.js +369 -0
  135. package/dist-server/service/apply-patch.js.map +1 -0
  136. package/dist-server/service/assistant.d.ts +84 -0
  137. package/dist-server/service/assistant.js +2237 -0
  138. package/dist-server/service/assistant.js.map +1 -0
  139. package/dist-server/service/board-ai-resolver.d.ts +54 -0
  140. package/dist-server/service/board-ai-resolver.js +409 -0
  141. package/dist-server/service/board-ai-resolver.js.map +1 -0
  142. package/dist-server/service/catalog-resolver.d.ts +18 -0
  143. package/dist-server/service/catalog-resolver.js +98 -0
  144. package/dist-server/service/catalog-resolver.js.map +1 -0
  145. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
  146. package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
  147. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
  148. package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
  149. package/dist-server/service/chat-message/chat-message-publish.js +27 -0
  150. package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
  151. package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
  152. package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
  153. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
  154. package/dist-server/service/chat-message/chat-message.d.ts +56 -0
  155. package/dist-server/service/chat-message/chat-message.js +149 -0
  156. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  157. package/dist-server/service/chat-message/index.d.ts +5 -0
  158. package/dist-server/service/chat-message/index.js +12 -0
  159. package/dist-server/service/chat-message/index.js.map +1 -0
  160. package/dist-server/service/chat-session/chat-session.d.ts +31 -0
  161. package/dist-server/service/chat-session/chat-session.js +131 -0
  162. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  163. package/dist-server/service/chat-session/index.d.ts +3 -0
  164. package/dist-server/service/chat-session/index.js +7 -0
  165. package/dist-server/service/chat-session/index.js.map +1 -0
  166. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
  167. package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
  168. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
  169. package/dist-server/service/chat-session-participant/index.d.ts +4 -0
  170. package/dist-server/service/chat-session-participant/index.js +10 -0
  171. package/dist-server/service/chat-session-participant/index.js.map +1 -0
  172. package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
  173. package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
  174. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
  175. package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
  176. package/dist-server/service/chat-session-participant/presence.js +22 -0
  177. package/dist-server/service/chat-session-participant/presence.js.map +1 -0
  178. package/dist-server/service/chat-session-resolver.d.ts +20 -0
  179. package/dist-server/service/chat-session-resolver.js +356 -0
  180. package/dist-server/service/chat-session-resolver.js.map +1 -0
  181. package/dist-server/service/component-tree.d.ts +68 -0
  182. package/dist-server/service/component-tree.js +73 -0
  183. package/dist-server/service/component-tree.js.map +1 -0
  184. package/dist-server/service/index.d.ts +20 -0
  185. package/dist-server/service/index.js +34 -0
  186. package/dist-server/service/index.js.map +1 -0
  187. package/dist-server/service/mention-marker.d.ts +78 -0
  188. package/dist-server/service/mention-marker.js +162 -0
  189. package/dist-server/service/mention-marker.js.map +1 -0
  190. package/dist-server/service/mention-resolver.d.ts +96 -0
  191. package/dist-server/service/mention-resolver.js +177 -0
  192. package/dist-server/service/mention-resolver.js.map +1 -0
  193. package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
  194. package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
  195. package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
  196. package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
  197. package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
  198. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
  199. package/dist-server/service/patch-entry/index.d.ts +4 -0
  200. package/dist-server/service/patch-entry/index.js +12 -0
  201. package/dist-server/service/patch-entry/index.js.map +1 -0
  202. package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
  203. package/dist-server/service/patch-entry/patch-entry.js +96 -0
  204. package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
  205. package/dist-server/service/prefab-registry.d.ts +73 -0
  206. package/dist-server/service/prefab-registry.js +258 -0
  207. package/dist-server/service/prefab-registry.js.map +1 -0
  208. package/dist-server/service/styling/clear-tools.d.ts +15 -0
  209. package/dist-server/service/styling/clear-tools.js +53 -0
  210. package/dist-server/service/styling/clear-tools.js.map +1 -0
  211. package/dist-server/service/styling/color-utils.d.ts +43 -0
  212. package/dist-server/service/styling/color-utils.js +210 -0
  213. package/dist-server/service/styling/color-utils.js.map +1 -0
  214. package/dist-server/service/styling/copy-tools.d.ts +27 -0
  215. package/dist-server/service/styling/copy-tools.js +81 -0
  216. package/dist-server/service/styling/copy-tools.js.map +1 -0
  217. package/dist-server/service/styling/effect-tools.d.ts +45 -0
  218. package/dist-server/service/styling/effect-tools.js +90 -0
  219. package/dist-server/service/styling/effect-tools.js.map +1 -0
  220. package/dist-server/service/styling/fill-tools.d.ts +60 -0
  221. package/dist-server/service/styling/fill-tools.js +229 -0
  222. package/dist-server/service/styling/fill-tools.js.map +1 -0
  223. package/dist-server/service/styling/material-tools.d.ts +32 -0
  224. package/dist-server/service/styling/material-tools.js +92 -0
  225. package/dist-server/service/styling/material-tools.js.map +1 -0
  226. package/dist-server/service/styling/read-tools.d.ts +34 -0
  227. package/dist-server/service/styling/read-tools.js +45 -0
  228. package/dist-server/service/styling/read-tools.js.map +1 -0
  229. package/dist-server/service/styling/registry.d.ts +91 -0
  230. package/dist-server/service/styling/registry.js +352 -0
  231. package/dist-server/service/styling/registry.js.map +1 -0
  232. package/dist-server/service/styling/stroke-tools.d.ts +35 -0
  233. package/dist-server/service/styling/stroke-tools.js +97 -0
  234. package/dist-server/service/styling/stroke-tools.js.map +1 -0
  235. package/dist-server/service/styling/text-tools.d.ts +32 -0
  236. package/dist-server/service/styling/text-tools.js +105 -0
  237. package/dist-server/service/styling/text-tools.js.map +1 -0
  238. package/dist-server/service/subscribe-auth.d.ts +14 -0
  239. package/dist-server/service/subscribe-auth.js +33 -0
  240. package/dist-server/service/subscribe-auth.js.map +1 -0
  241. package/dist-server/service/types.d.ts +265 -0
  242. package/dist-server/service/types.js +3 -0
  243. package/dist-server/service/types.js.map +1 -0
  244. package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
  245. package/dist-server/service/validation/board-model-schema.js +409 -0
  246. package/dist-server/service/validation/board-model-schema.js.map +1 -0
  247. package/dist-server/service/validation/tool-validation.d.ts +31 -0
  248. package/dist-server/service/validation/tool-validation.js +196 -0
  249. package/dist-server/service/validation/tool-validation.js.map +1 -0
  250. package/dist-server/tsconfig.tsbuildinfo +1 -0
  251. package/package.json +48 -0
  252. package/server/index.ts +23 -0
  253. package/server/service/agentic-loop.test.ts +705 -0
  254. package/server/service/agentic-loop.ts +431 -0
  255. package/server/service/apply-patch-drift.test.ts +829 -0
  256. package/server/service/apply-patch.test.ts +757 -0
  257. package/server/service/apply-patch.ts +411 -0
  258. package/server/service/assistant-integration.test.ts +922 -0
  259. package/server/service/assistant-llm-smoke.test.ts +296 -0
  260. package/server/service/assistant.test.ts +2034 -0
  261. package/server/service/assistant.ts +2378 -0
  262. package/server/service/board-ai-resolver.ts +407 -0
  263. package/server/service/catalog-resolver.test.ts +137 -0
  264. package/server/service/catalog-resolver.ts +102 -0
  265. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
  266. package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
  267. package/server/service/chat-message/chat-message-broadcast.ts +27 -0
  268. package/server/service/chat-message/chat-message-publish.ts +22 -0
  269. package/server/service/chat-message/chat-message-subscription.ts +41 -0
  270. package/server/service/chat-message/chat-message.ts +169 -0
  271. package/server/service/chat-message/index.ts +12 -0
  272. package/server/service/chat-session/chat-session.ts +130 -0
  273. package/server/service/chat-session/index.ts +5 -0
  274. package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
  275. package/server/service/chat-session-participant/index.ts +6 -0
  276. package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
  277. package/server/service/chat-session-participant/invite-notification.ts +48 -0
  278. package/server/service/chat-session-participant/presence.test.ts +39 -0
  279. package/server/service/chat-session-participant/presence.ts +21 -0
  280. package/server/service/chat-session-resolver.ts +312 -0
  281. package/server/service/component-tree.test.ts +248 -0
  282. package/server/service/component-tree.ts +135 -0
  283. package/server/service/index.ts +32 -0
  284. package/server/service/mention-marker.extract.test.ts +61 -0
  285. package/server/service/mention-marker.test.ts +228 -0
  286. package/server/service/mention-marker.ts +184 -0
  287. package/server/service/mention-resolver.test.ts +273 -0
  288. package/server/service/mention-resolver.ts +264 -0
  289. package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
  290. package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
  291. package/server/service/patch-entry/board-patch-subscription.ts +50 -0
  292. package/server/service/patch-entry/index.ts +12 -0
  293. package/server/service/patch-entry/patch-entry.ts +89 -0
  294. package/server/service/prefab-registry.test.ts +172 -0
  295. package/server/service/prefab-registry.ts +332 -0
  296. package/server/service/styling/clear-tools.test.ts +164 -0
  297. package/server/service/styling/clear-tools.ts +69 -0
  298. package/server/service/styling/color-utils.test.ts +130 -0
  299. package/server/service/styling/color-utils.ts +226 -0
  300. package/server/service/styling/copy-tools.test.ts +152 -0
  301. package/server/service/styling/copy-tools.ts +115 -0
  302. package/server/service/styling/effect-tools.test.ts +277 -0
  303. package/server/service/styling/effect-tools.ts +124 -0
  304. package/server/service/styling/fill-tools.test.ts +331 -0
  305. package/server/service/styling/fill-tools.ts +293 -0
  306. package/server/service/styling/material-tools.test.ts +294 -0
  307. package/server/service/styling/material-tools.ts +114 -0
  308. package/server/service/styling/read-tools.test.ts +78 -0
  309. package/server/service/styling/read-tools.ts +67 -0
  310. package/server/service/styling/registry.test.ts +203 -0
  311. package/server/service/styling/registry.ts +423 -0
  312. package/server/service/styling/stroke-tools.test.ts +245 -0
  313. package/server/service/styling/stroke-tools.ts +145 -0
  314. package/server/service/styling/text-tools.test.ts +253 -0
  315. package/server/service/styling/text-tools.ts +131 -0
  316. package/server/service/styling/tier1-tools.test.ts +300 -0
  317. package/server/service/subscribe-auth.test.ts +57 -0
  318. package/server/service/subscribe-auth.ts +34 -0
  319. package/server/service/types.ts +235 -0
  320. package/server/service/validation/board-model-schema.test.ts +778 -0
  321. package/server/service/validation/board-model-schema.ts +446 -0
  322. package/server/service/validation/tool-validation.ts +228 -0
  323. package/things-factory.config.js +1 -0
  324. package/translations/en.json +49 -0
  325. package/translations/ja.json +48 -0
  326. package/translations/ko.json +49 -0
  327. package/translations/ms.json +48 -0
  328. package/translations/zh.json +48 -0
  329. 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
+ }