@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,73 @@
1
+ /**
2
+ * chat-message 방송 spine — 독립 통합 테스트 (실서버·WS 없이 자족적).
3
+ *
4
+ * 순수 필터(chat-message-broadcast.test.ts)와 달리, 실제 서버가 쓰는 동일한 경로를 구동:
5
+ * publishChatMessage → shell pubsub(in-memory) → 토픽 → chatMessageFilter → async iterator.
6
+ * = resolver 의 subscribe 합성과 동일. WS/Apollo 전송 계층은 patch spine(C2)에서 검증된
7
+ * generic 인프라라 본 테스트 범위 밖 (수동 스모크로 1회 확인).
8
+ */
9
+ import { filter, pipe } from 'graphql-yoga'
10
+ import { pubsub } from '@things-factory/shell'
11
+ import { publishChatMessage } from './chat-message-publish'
12
+ import { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast'
13
+
14
+ const msg = (id: string, role = 'user', content = 'hi') => ({ id, role, content } as any)
15
+
16
+ /** resolver 의 subscribe 와 동일한 합성으로 구독 이터레이터를 만든다. */
17
+ function subscribe(domainId: string, sessionId: string) {
18
+ return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domainId, sessionId)))[
19
+ Symbol.asyncIterator
20
+ ]()
21
+ }
22
+
23
+ /** it.next() 를 timeout 과 race — 안 오면 'TIMEOUT'. */
24
+ function nextOrTimeout(it: AsyncIterator<any>, ms = 400): Promise<any> {
25
+ return Promise.race([
26
+ it.next().then(r => r.value),
27
+ new Promise(resolve => setTimeout(() => resolve('TIMEOUT'), ms))
28
+ ])
29
+ }
30
+
31
+ describe('chat-message 방송 통합 (publish → 필터 → 구독)', () => {
32
+ it('같은 domain+session 구독자가 방송 메시지를 수신', async () => {
33
+ const it = subscribe('d1', 's1')
34
+ const p = nextOrTimeout(it)
35
+ publishChatMessage({ domainId: 'd1', sessionId: 's1', message: msg('m1', 'assistant', 'done') })
36
+ const value = await p
37
+ expect(value).not.toBe('TIMEOUT')
38
+ expect(value.message.id).toBe('m1')
39
+ expect(value.message.role).toBe('assistant')
40
+ await it.return?.()
41
+ })
42
+
43
+ it('다른 세션 메시지는 격리 — 내 세션 것만 수신', async () => {
44
+ const it = subscribe('d1', 's1')
45
+ const p = nextOrTimeout(it)
46
+ publishChatMessage({ domainId: 'd1', sessionId: 's2', message: msg('other') }) // 다른 세션 — 필터됨
47
+ publishChatMessage({ domainId: 'd1', sessionId: 's1', message: msg('mine') }) // 내 세션
48
+ const value = await p
49
+ expect(value).not.toBe('TIMEOUT')
50
+ expect(value.message.id).toBe('mine')
51
+ await it.return?.()
52
+ })
53
+
54
+ it('다른 domain 메시지는 격리 (도메인 보안)', async () => {
55
+ const it = subscribe('d1', 's1')
56
+ const p = nextOrTimeout(it)
57
+ publishChatMessage({ domainId: 'd2', sessionId: 's1', message: msg('foreign') }) // 다른 도메인 — 필터됨
58
+ publishChatMessage({ domainId: 'd1', sessionId: 's1', message: msg('local') })
59
+ const value = await p
60
+ expect(value).not.toBe('TIMEOUT')
61
+ expect(value.message.id).toBe('local')
62
+ await it.return?.()
63
+ })
64
+
65
+ it('sessionId 없는 publish 는 방송 안 함 (구독자 미수신)', async () => {
66
+ const it = subscribe('d1', 's1')
67
+ const p = nextOrTimeout(it, 250)
68
+ publishChatMessage({ domainId: 'd1', sessionId: undefined, message: msg('nope') })
69
+ const value = await p
70
+ expect(value).toBe('TIMEOUT') // 아무것도 안 옴
71
+ await it.return?.()
72
+ })
73
+ })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * chat-message 브로드캐스트 — 단위 테스트 (순수 로직).
3
+ * - chatMessageFilter: domain+session 격리(보안 핵심) — 일치만 통과, 불일치/널/session없음 차단.
4
+ * (publishChatMessage / 구독 resolver 는 pubsub·graphql-ws 통합 영역 — 별도 검증.)
5
+ */
6
+ import { chatMessageFilter, CHAT_MESSAGE_TOPIC } from './chat-message-broadcast'
7
+ import type { ChatMessagePayload } from './chat-message-broadcast'
8
+
9
+ const payload = (domainId: string, sessionId?: string): ChatMessagePayload => ({
10
+ domainId,
11
+ sessionId,
12
+ message: { id: 'm1' } as any
13
+ })
14
+
15
+ describe('chatMessageFilter (domain+session 격리)', () => {
16
+ it('domain+session 모두 일치 → 통과', () => {
17
+ expect(chatMessageFilter('d1', 's1')(payload('d1', 's1'))).toBe(true)
18
+ })
19
+
20
+ it('domain 불일치 → 차단 (도메인 격리)', () => {
21
+ expect(chatMessageFilter('d1', 's1')(payload('d2', 's1'))).toBe(false)
22
+ })
23
+
24
+ it('session 불일치 → 차단 (다른 세션 메시지 누수 방지)', () => {
25
+ expect(chatMessageFilter('d1', 's1')(payload('d1', 's2'))).toBe(false)
26
+ })
27
+
28
+ it('sessionId 없는 페이로드 → 차단', () => {
29
+ expect(chatMessageFilter('d1', 's1')(payload('d1', undefined))).toBe(false)
30
+ })
31
+
32
+ it('null/undefined 페이로드 안전 → 차단', () => {
33
+ expect(chatMessageFilter('d1', 's1')(null as any)).toBe(false)
34
+ expect(chatMessageFilter('d1', 's1')(undefined as any)).toBe(false)
35
+ })
36
+
37
+ it('토픽 상수', () => {
38
+ expect(CHAT_MESSAGE_TOPIC).toBe('board-ai-chat-message')
39
+ })
40
+ })
@@ -0,0 +1,27 @@
1
+ /*
2
+ * chat-message 브로드캐스트 — 순수 로직 (토픽/페이로드/필터). 데코레이터·pubsub import 없음
3
+ * → 단위테스트 가능. 데코레이터 resolver(chat-message-subscription.ts)와 pubsub 의존
4
+ * publishChatMessage 는 별도.
5
+ *
6
+ * patch spine(board-patch-broadcast.ts)과 동일 컨벤션. 다만 채팅 메시지는 board 가 아니라
7
+ * **ChatSession 단위**로 묶이므로 필터 키가 sessionId (+ domain 격리).
8
+ */
9
+ import type { ChatMessage } from './chat-message.js' // type-only — 런타임 import 아님
10
+
11
+ /** chat-message 브로드캐스트 토픽. */
12
+ export const CHAT_MESSAGE_TOPIC = 'board-ai-chat-message'
13
+
14
+ /** 방송 페이로드 — 구독 필터(domain+session)용 메타 + ChatMessage 본체. */
15
+ export interface ChatMessagePayload {
16
+ domainId: string
17
+ sessionId?: string
18
+ message: ChatMessage
19
+ }
20
+
21
+ /**
22
+ * 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).
23
+ */
24
+ export function chatMessageFilter(domainId: string, sessionId: string) {
25
+ return (payload: ChatMessagePayload): boolean =>
26
+ !!payload && payload.domainId === domainId && payload.sessionId === sessionId
27
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * chat-message 방송 publisher — pubsub 의존, 데코레이터 없음.
3
+ *
4
+ * 데코레이터 resolver(chat-message-subscription.ts)와 분리한 이유: babel-jest 가 파라미터
5
+ * 데코레이터(@Root/@Arg)를 파싱 못 해 그 파일을 테스트에서 import 할 수 없다. 이 모듈은
6
+ * 데코레이터가 없어 통합 테스트가 실제 publish→방송 경로를 직접 구동할 수 있다.
7
+ */
8
+ import { pubsub } from '@things-factory/shell'
9
+ import { CHAT_MESSAGE_TOPIC, type ChatMessagePayload } from './chat-message-broadcast.js'
10
+
11
+ /**
12
+ * ChatMessage 영속 직후 호출 — 같은 세션 구독자에게 방송.
13
+ * sessionId 없으면 skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.
14
+ */
15
+ export function publishChatMessage(payload: ChatMessagePayload): void {
16
+ if (!payload.sessionId) return
17
+ try {
18
+ pubsub.publish(CHAT_MESSAGE_TOPIC, payload)
19
+ } catch {
20
+ /* 방송 생략 — 영속은 이미 끝남 */
21
+ }
22
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ * BoardChatMessageSubscription (Phase Board-AI C4 — 그룹챗 메시지 실시간 방송).
3
+ *
4
+ * ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →
5
+ * 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.
6
+ * 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).
7
+ *
8
+ * patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.
9
+ * 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).
10
+ * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.
11
+ */
12
+ import { Resolver, Subscription, Root, Arg } from 'type-graphql'
13
+ import { filter, pipe } from 'graphql-yoga'
14
+ import { pubsub } from '@things-factory/shell'
15
+ import { ChatMessage } from './chat-message.js'
16
+ import { CHAT_MESSAGE_TOPIC, chatMessageFilter, type ChatMessagePayload } from './chat-message-broadcast.js'
17
+ import { assertDomainSubscribeAllowed } from '../subscribe-auth.js'
18
+
19
+ // publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).
20
+ export { publishChatMessage } from './chat-message-publish.js'
21
+ export { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js'
22
+ export type { ChatMessagePayload } from './chat-message-broadcast.js'
23
+
24
+ @Resolver(ChatMessage)
25
+ export class BoardChatMessageSubscription {
26
+ @Subscription({
27
+ description:
28
+ 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',
29
+ subscribe: async ({ args, context }) => {
30
+ const { domain, user } = context.state
31
+ const { sessionId } = args
32
+
33
+ await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))
34
+
35
+ return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domain.id, sessionId)))
36
+ }
37
+ })
38
+ chatMessageActivity(@Root() payload: ChatMessagePayload, @Arg('sessionId') sessionId: string): ChatMessage {
39
+ return payload.message
40
+ }
41
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ChatMessage — ChatSession 의 한 메시지.
3
+ *
4
+ * role: 'user' | 'assistant' | 'system'
5
+ * 'system' 은 사용자 직접 편집/import 진행 등 시스템이 자동 생성하는 컨텍스트 메시지.
6
+ *
7
+ * relatedPatchId / relatedImportSessionId 로 메시지가 트리거한 패치·임포트와 연결.
8
+ */
9
+ import {
10
+ Column,
11
+ CreateDateColumn,
12
+ DeleteDateColumn,
13
+ Entity,
14
+ Index,
15
+ ManyToOne,
16
+ PrimaryGeneratedColumn,
17
+ RelationId,
18
+ UpdateDateColumn
19
+ } from 'typeorm'
20
+ import { Field, ID, ObjectType } from 'type-graphql'
21
+ import { GraphQLJSON } from 'graphql-scalars'
22
+
23
+ import { User } from '@things-factory/auth-base'
24
+ import { config } from '@things-factory/env'
25
+
26
+ import { ChatSession } from '../chat-session/chat-session.js'
27
+
28
+ const ORMCONFIG = config.get('ormconfig', {})
29
+ const DATABASE_TYPE = ORMCONFIG.type
30
+
31
+ @Entity()
32
+ @Index('ix_chat_message_1', (message: ChatMessage) => [message.session, message.createdAt])
33
+ @ObjectType({ description: 'A single chat message in a ChatSession.' })
34
+ export class ChatMessage {
35
+ @PrimaryGeneratedColumn('uuid')
36
+ @Field(type => ID, { nullable: true })
37
+ readonly id?: string
38
+
39
+ @ManyToOne(type => ChatSession, session => session.messages, { onDelete: 'CASCADE' })
40
+ session?: ChatSession
41
+
42
+ @RelationId((message: ChatMessage) => message.session)
43
+ sessionId?: string
44
+
45
+ @Column({ type: 'varchar', length: 16 })
46
+ @Field({ description: "'user' | 'assistant' | 'system'" })
47
+ role!: string
48
+
49
+ @Column({
50
+ type:
51
+ DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
52
+ ? 'longtext'
53
+ : DATABASE_TYPE == 'oracle'
54
+ ? 'clob'
55
+ : DATABASE_TYPE == 'mssql'
56
+ ? 'nvarchar'
57
+ : 'text',
58
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
59
+ })
60
+ @Field({ description: 'Message content' })
61
+ content!: string
62
+
63
+ @Column({ nullable: true })
64
+ @Field({ nullable: true, description: 'PatchEntry.id this message triggered (if any).' })
65
+ relatedPatchId?: string
66
+
67
+ @Column({ nullable: true })
68
+ @Field({ nullable: true, description: 'ImportSession.id this message triggered (if any).' })
69
+ relatedImportSessionId?: string
70
+
71
+ /**
72
+ * AI 가 응답을 만드는 동안 호출한 도구 trace — JSON-stringified.
73
+ *
74
+ * 컬럼은 string (PatchEntry.ops 와 동일 패턴), GraphQL 노출은 toolUsagesJson getter
75
+ * 로 parse 된 객체. UI 의 fold-able 박스 ("AI 가 이런 도구를 사용했습니다") 가
76
+ * 페이지 reload / loadHistory 후에도 살아있도록 영속.
77
+ *
78
+ * 형태: ToolUsage[] (assistant.ts 의 시간순 trace, summarizeToolResult 로 압축).
79
+ * null 이면 도구 호출 없었던 메시지 (text-only 응답).
80
+ */
81
+ @Column({
82
+ type:
83
+ DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
84
+ ? 'longtext'
85
+ : DATABASE_TYPE == 'oracle'
86
+ ? 'clob'
87
+ : DATABASE_TYPE == 'mssql'
88
+ ? 'nvarchar'
89
+ : 'text',
90
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined,
91
+ nullable: true
92
+ })
93
+ toolUsages?: string
94
+
95
+ /** GraphQL 노출용 — JSON 배열로 parse. */
96
+ @Field(() => GraphQLJSON, {
97
+ nullable: true,
98
+ description:
99
+ 'Tool usage trace — array of {name, arguments, result, kind}. Only on assistant messages.'
100
+ })
101
+ get toolUsagesJson(): any {
102
+ if (!this.toolUsages) return null
103
+ try {
104
+ return JSON.parse(this.toolUsages)
105
+ } catch {
106
+ return null
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 메시지를 일으킨 사용자.
112
+ * - role='user' → 보낸 사용자
113
+ * - role='assistant' → 그 응답을 트리거한 사용자 (직전 user 메시지의 작성자)
114
+ * - role='system' → 시스템 액션을 일으킨 사용자 (예: 직접 편집한 사용자)
115
+ *
116
+ * nullable — legacy 메시지 (이 필드 추가 이전에 작성) 는 null. 또한 ad-hoc 모드 / 시스템
117
+ * 자동 메시지 중 사용자 컨텍스트가 없는 경우도 null 가능.
118
+ */
119
+ @ManyToOne(type => User, { nullable: true })
120
+ @Field(type => User, { nullable: true, description: 'User who triggered this message.' })
121
+ creator?: User
122
+
123
+ @RelationId((message: ChatMessage) => message.creator)
124
+ creatorId?: string
125
+
126
+ @ManyToOne(type => User, { nullable: true })
127
+ @Field(type => User, { nullable: true, description: 'Last user to update this message (for future edit support).' })
128
+ updater?: User
129
+
130
+ @RelationId((message: ChatMessage) => message.updater)
131
+ updaterId?: string
132
+
133
+ /**
134
+ * 부모 메시지 — 이 메시지가 "응답하는 대상" / "이어가는 메시지" 의 id.
135
+ *
136
+ * 의미 (role 별):
137
+ * - 'user' → 직전 in-session 메시지 (보통 assistant 또는 system). 첫 turn 은 null.
138
+ * - 'assistant' → 이 응답이 답하는 user 메시지. (regenerate 로 같은 user 메시지를 부모로
139
+ * 하는 형제 assistant 메시지가 생기면 자연스럽게 branching tree 가 됨)
140
+ * - 'system' → 직전 in-session 메시지 (timeline 연속성 유지).
141
+ *
142
+ * 이 한 컬럼으로 향후 어떤 흐름이든 표현 가능 — linear chain / branching (regenerate) /
143
+ * tree (replies-to-specific-message) 등. UI/쿼리는 점진 도입.
144
+ *
145
+ * onDelete: 'SET NULL' — 부모 메시지가 삭제돼도 child 는 남되 dangling 마킹.
146
+ */
147
+ @ManyToOne(type => ChatMessage, { nullable: true, onDelete: 'SET NULL' })
148
+ parentMessage?: ChatMessage
149
+
150
+ @RelationId((message: ChatMessage) => message.parentMessage)
151
+ @Field({
152
+ nullable: true,
153
+ description:
154
+ 'Id of the message this one is in-response-to / continues. Enables linear / branching / tree structures.'
155
+ })
156
+ parentMessageId?: string
157
+
158
+ @CreateDateColumn()
159
+ @Field({ nullable: true, description: 'Creation timestamp.' })
160
+ createdAt?: Date
161
+
162
+ @UpdateDateColumn()
163
+ @Field({ nullable: true, description: 'Last update timestamp (relevant once message editing is supported).' })
164
+ updatedAt?: Date
165
+
166
+ @DeleteDateColumn()
167
+ @Field({ nullable: true, description: 'Soft delete timestamp.' })
168
+ deletedAt?: Date
169
+ }
@@ -0,0 +1,12 @@
1
+ import { ChatMessage } from './chat-message.js'
2
+
3
+ export { ChatMessage }
4
+ export {
5
+ BoardChatMessageSubscription,
6
+ publishChatMessage,
7
+ CHAT_MESSAGE_TOPIC,
8
+ chatMessageFilter
9
+ } from './chat-message-subscription.js'
10
+ export type { ChatMessagePayload } from './chat-message-subscription.js'
11
+
12
+ export const entities = [ChatMessage]
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ChatSession — Board 와 결합되는 AI 협력 세션.
3
+ *
4
+ * 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.
5
+ *
6
+ * 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /
7
+ * archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N
8
+ * 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`
9
+ * 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.
10
+ *
11
+ * 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)
12
+ * 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.
13
+ *
14
+ * board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).
15
+ */
16
+ import {
17
+ Column,
18
+ CreateDateColumn,
19
+ DeleteDateColumn,
20
+ Entity,
21
+ Index,
22
+ ManyToOne,
23
+ OneToMany,
24
+ PrimaryGeneratedColumn,
25
+ RelationId,
26
+ UpdateDateColumn
27
+ } from 'typeorm'
28
+ import { Field, ID, ObjectType } from 'type-graphql'
29
+
30
+ import { Domain } from '@things-factory/shell'
31
+ import { User } from '@things-factory/auth-base'
32
+ import { config } from '@things-factory/env'
33
+
34
+ import { ChatMessage } from '../chat-message/chat-message.js'
35
+ import { PatchEntry } from '../patch-entry/patch-entry.js'
36
+
37
+ const ORMCONFIG = config.get('ormconfig', {})
38
+ const DATABASE_TYPE = ORMCONFIG.type
39
+
40
+ @Entity()
41
+ // 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).
42
+ // unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.
43
+ @Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])
44
+ @ObjectType({
45
+ description: 'AI 협력 세션 — Board 와 결합. 한 보드에 여러 세션 가능 (thread / 사용자별 / 컨텍스트별 등 미래 확장).'
46
+ })
47
+ export class ChatSession {
48
+ @PrimaryGeneratedColumn('uuid')
49
+ @Field(type => ID, { nullable: true })
50
+ readonly id?: string
51
+
52
+ @ManyToOne(type => Domain)
53
+ @Field(type => Domain, { nullable: true })
54
+ domain?: Domain
55
+
56
+ @RelationId((session: ChatSession) => session.domain)
57
+ domainId?: string
58
+
59
+ /** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능. */
60
+ @Column({ nullable: true })
61
+ @Field({
62
+ nullable: true,
63
+ description:
64
+ 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts).'
65
+ })
66
+ boardId?: string
67
+
68
+ /**
69
+ * 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).
70
+ *
71
+ * nullable — 미설정 시 UI 가 fallback 라벨 ("세션 N" 또는 createdAt 기반) 사용. 사용자가
72
+ * 다중 세션 운영 시 의미 부여 (예: "초기 설계", "운영 시뮬레이션", "사용자A 의 사적 세션").
73
+ */
74
+ @Column({ nullable: true, length: 200 })
75
+ @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })
76
+ name?: string
77
+
78
+ @ManyToOne(type => User)
79
+ @Field(type => User, { nullable: true, description: 'User who created this session.' })
80
+ creator?: User
81
+
82
+ @RelationId((session: ChatSession) => session.creator)
83
+ creatorId?: string
84
+
85
+ @ManyToOne(type => User)
86
+ @Field(type => User, { nullable: true, description: 'User who last updated this session.' })
87
+ updater?: User
88
+
89
+ @RelationId((session: ChatSession) => session.updater)
90
+ updaterId?: string
91
+
92
+ /** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */
93
+ @Column({
94
+ nullable: true,
95
+ type:
96
+ DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
97
+ ? 'longtext'
98
+ : DATABASE_TYPE == 'oracle'
99
+ ? 'clob'
100
+ : DATABASE_TYPE == 'mssql'
101
+ ? 'nvarchar'
102
+ : 'text',
103
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
104
+ })
105
+ @Field({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })
106
+ lastSummary?: string
107
+
108
+ /** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */
109
+ @Column({ nullable: true })
110
+ @Field({ nullable: true })
111
+ aiClientId?: string
112
+
113
+ @CreateDateColumn()
114
+ @Field({ nullable: true })
115
+ createdAt?: Date
116
+
117
+ @UpdateDateColumn()
118
+ @Field({ nullable: true })
119
+ updatedAt?: Date
120
+
121
+ @DeleteDateColumn()
122
+ deletedAt?: Date
123
+
124
+ // ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──
125
+ @OneToMany(type => ChatMessage, message => message.session)
126
+ messages?: ChatMessage[]
127
+
128
+ @OneToMany(type => PatchEntry, patch => patch.session)
129
+ patches?: PatchEntry[]
130
+ }
@@ -0,0 +1,5 @@
1
+ import { ChatSession } from './chat-session.js'
2
+
3
+ export { ChatSession }
4
+
5
+ export const entities = [ChatSession]
@@ -0,0 +1,110 @@
1
+ /**
2
+ * ChatSessionParticipant — 채팅 세션의 참여 사용자.
3
+ *
4
+ * 한 ChatSession 에 여러 사용자가 참여할 수 있고, 각자 role (owner / member / viewer 등)
5
+ * 을 가짐. 다자간 채팅 / 권한 분화 / "내 세션 목록" / 멤버 표시 등의 기반.
6
+ *
7
+ * 현재 visibility 정책 (도메인 내 모두 공개) 은 그대로 유지 — 본 엔티티는 영속/추적
8
+ * 데이터 만들고, "참여자만 접근" 같은 enforcement 는 정책 굳을 때 별도 phase 에서 도입.
9
+ *
10
+ * 자동 참여 vs 명시적 초대 정책도 미정 — 일단 세션 creator 만 자동 'owner' 로 등록.
11
+ */
12
+ import {
13
+ Column,
14
+ CreateDateColumn,
15
+ DeleteDateColumn,
16
+ Entity,
17
+ Index,
18
+ ManyToOne,
19
+ PrimaryGeneratedColumn,
20
+ RelationId,
21
+ UpdateDateColumn
22
+ } from 'typeorm'
23
+ import { Field, ID, ObjectType } from 'type-graphql'
24
+
25
+ import { Domain } from '@things-factory/shell'
26
+ import { User } from '@things-factory/auth-base'
27
+
28
+ import { ChatSession } from '../chat-session/chat-session.js'
29
+
30
+ @Entity('chat_session_participants')
31
+ @Index('ix_chat_session_participant_0', ['domain', 'session', 'user'], {
32
+ unique: true,
33
+ where: '"deleted_at" IS NULL'
34
+ })
35
+ @Index('ix_chat_session_participant_1', ['domain', 'user'], {
36
+ where: '"deleted_at" IS NULL'
37
+ })
38
+ @ObjectType({
39
+ description:
40
+ 'Participant of a ChatSession — links a User to a session with a role. Foundation for multi-user chat / per-user filtering / member display.'
41
+ })
42
+ export class ChatSessionParticipant {
43
+ @PrimaryGeneratedColumn('uuid')
44
+ @Field(type => ID, { description: 'Unique participant id.' })
45
+ readonly id?: string
46
+
47
+ @ManyToOne(type => Domain)
48
+ @Field(type => Domain, { nullable: true, description: 'Domain to which this participant belongs.' })
49
+ domain?: Domain
50
+
51
+ @RelationId((p: ChatSessionParticipant) => p.domain)
52
+ domainId?: string
53
+
54
+ @ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })
55
+ @Field(type => ChatSession, { nullable: true, description: 'The ChatSession this participation belongs to.' })
56
+ session?: ChatSession
57
+
58
+ @RelationId((p: ChatSessionParticipant) => p.session)
59
+ sessionId?: string
60
+
61
+ @ManyToOne(type => User)
62
+ @Field(type => User, { nullable: true, description: 'The user participating in the session.' })
63
+ user?: User
64
+
65
+ @RelationId((p: ChatSessionParticipant) => p.user)
66
+ userId?: string
67
+
68
+ /**
69
+ * 참여자의 역할.
70
+ * - 'owner': 세션 만든 사람. rename / delete 권한.
71
+ * - 'member': 일반 참여자. 메시지 송수신.
72
+ * - 'viewer': 읽기 전용 (미래 권한 정책 활성화 시).
73
+ *
74
+ * 시작은 'owner' / 'member' 둘로 충분. 더 세분화 필요해지면 enum 확장.
75
+ */
76
+ @Column({ type: 'varchar', length: 16, default: 'member' })
77
+ @Field({ description: 'Role of this participant: owner | member | viewer.' })
78
+ role!: string
79
+
80
+ /** 마지막 활동 시각 — presence indicator. nullable. */
81
+ @Column({ nullable: true })
82
+ @Field({ nullable: true, description: 'Last seen / activity timestamp (presence indicator).' })
83
+ lastSeenAt?: Date
84
+
85
+ @ManyToOne(type => User)
86
+ @Field(type => User, { nullable: true, description: 'User who created this participant record (typically session owner who invited).' })
87
+ creator?: User
88
+
89
+ @RelationId((p: ChatSessionParticipant) => p.creator)
90
+ creatorId?: string
91
+
92
+ @ManyToOne(type => User)
93
+ @Field(type => User, { nullable: true, description: 'Last user to update this participant record (e.g., role change).' })
94
+ updater?: User
95
+
96
+ @RelationId((p: ChatSessionParticipant) => p.updater)
97
+ updaterId?: string
98
+
99
+ @CreateDateColumn()
100
+ @Field({ nullable: true, description: 'Joined-at timestamp.' })
101
+ createdAt?: Date
102
+
103
+ @UpdateDateColumn()
104
+ @Field({ nullable: true, description: 'Last update timestamp (role change / lastSeenAt bump).' })
105
+ updatedAt?: Date
106
+
107
+ @DeleteDateColumn()
108
+ @Field({ nullable: true, description: 'Soft delete (= left session) timestamp.' })
109
+ deletedAt?: Date
110
+ }
@@ -0,0 +1,6 @@
1
+ import { ChatSessionParticipant } from './chat-session-participant.js'
2
+
3
+ export { ChatSessionParticipant }
4
+ export { isOnline, PRESENCE_TTL_MS } from './presence.js'
5
+
6
+ export const entities = [ChatSessionParticipant]
@@ -0,0 +1,30 @@
1
+ import { buildInviteNotification } from './invite-notification'
2
+
3
+ describe('buildInviteNotification — 협업 초대 알림 payload', () => {
4
+ test('받는 사람 없으면 null', () => {
5
+ expect(buildInviteNotification([], 'board-1', { name: 'Kim' })).toBeNull()
6
+ expect(buildInviteNotification(undefined as any, 'board-1', { name: 'Kim' })).toBeNull()
7
+ })
8
+
9
+ test('정상 — receivers / type / mode 고정', () => {
10
+ const p = buildInviteNotification(['u1', 'u2'], 'board-9', { name: 'Kim' })!
11
+ expect(p).not.toBeNull()
12
+ expect(p.receivers).toEqual(['u1', 'u2'])
13
+ expect(p.type).toBe('board-collab-invite')
14
+ expect(p.subject).toBe('info')
15
+ expect(p.mode).toBe('background')
16
+ expect(p.title).toBeTruthy()
17
+ })
18
+
19
+ test('inviter 이름 우선, 없으면 email, 둘 다 없으면 someone', () => {
20
+ expect(buildInviteNotification(['u1'], 'b', { name: 'Kim', email: 'k@x.io' })!.body).toContain('Kim')
21
+ expect(buildInviteNotification(['u1'], 'b', { email: 'k@x.io' })!.body).toContain('k@x.io')
22
+ expect(buildInviteNotification(['u1'], 'b', {})!.body).toContain('someone')
23
+ expect(buildInviteNotification(['u1'], 'b', null)!.body).toContain('someone')
24
+ })
25
+
26
+ test('boardId 있으면 모델러 링크, 없으면 url 생략', () => {
27
+ expect(buildInviteNotification(['u1'], 'board-42', { name: 'Kim' })!.url).toBe('board-modeller/board-42')
28
+ expect(buildInviteNotification(['u1'], undefined, { name: 'Kim' })!.url).toBeUndefined()
29
+ })
30
+ })