@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,131 @@
1
+ /**
2
+ * setText / setFont — 컴포넌트의 텍스트 내용 / 폰트 스타일.
3
+ *
4
+ * setText: state.text 변경 (내용)
5
+ * setFont: state.fontFamily / fontSize / fontWeight / fontStyle / fontColor /
6
+ * textAlign / textBaseline / lineHeight 변경 (폰트 스타일)
7
+ *
8
+ * 두 tool 분리 — 의도가 다름. 내용 변경 vs 시각 스타일.
9
+ */
10
+ import type { BoardEditOp } from '../types'
11
+ import { normalizeColor, validateColor } from './color-utils'
12
+
13
+ export interface SetTextInput {
14
+ refids: number[]
15
+ text: string
16
+ }
17
+
18
+ export interface SetFontInput {
19
+ refids: number[]
20
+ fontFamily?: string
21
+ fontSize?: number
22
+ fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string | number
23
+ fontStyle?: 'normal' | 'italic' | 'oblique'
24
+ fontColor?: string
25
+ /** Canvas API 표준 — 'justify' 는 Canvas 미지원이라 제외. */
26
+ textAlign?: 'left' | 'center' | 'right' | 'start' | 'end'
27
+ textBaseline?: 'top' | 'middle' | 'bottom' | 'hanging' | 'alphabetic' | 'ideographic'
28
+ lineHeight?: number
29
+ }
30
+
31
+ // Canvas CanvasRenderingContext2D 가 허용하는 값만.
32
+ // dash / lineCap / lineJoin 과 동일 패턴 — AI 가 비표준 값 ('serif-italic',
33
+ // 'middle-align' 등) 을 보내도 Canvas 가 silent ignore → 효과 없는 op 가 그대로
34
+ // 실행되는 것 방지.
35
+ const VALID_FONT_STYLE = new Set(['normal', 'italic', 'oblique'])
36
+ const VALID_TEXT_ALIGN = new Set(['start', 'end', 'left', 'right', 'center'])
37
+ const VALID_TEXT_BASELINE = new Set([
38
+ 'top',
39
+ 'middle',
40
+ 'bottom',
41
+ 'hanging',
42
+ 'alphabetic',
43
+ 'ideographic'
44
+ ])
45
+
46
+ export interface OpsResult {
47
+ ops: BoardEditOp[]
48
+ errors: string[]
49
+ }
50
+
51
+ export function buildSetTextOps(input: SetTextInput): OpsResult {
52
+ if (!Array.isArray(input.refids) || input.refids.length === 0) {
53
+ return { ops: [], errors: ['refids must be a non-empty array'] }
54
+ }
55
+ if (typeof input.text !== 'string') {
56
+ return { ops: [], errors: ['text must be a string'] }
57
+ }
58
+ const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
59
+ if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
60
+
61
+ const patch = { text: input.text }
62
+ return {
63
+ ops: refids.map(refid => ({ op: 'modify', refid, patch })),
64
+ errors: []
65
+ }
66
+ }
67
+
68
+ export function buildSetFontOps(input: SetFontInput): OpsResult {
69
+ if (!Array.isArray(input.refids) || input.refids.length === 0) {
70
+ return { ops: [], errors: ['refids must be a non-empty array'] }
71
+ }
72
+ const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
73
+ if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
74
+
75
+ // 어느 인자도 없으면 무의미
76
+ const noOp = !(
77
+ input.fontFamily ||
78
+ input.fontSize !== undefined ||
79
+ input.fontWeight !== undefined ||
80
+ input.fontStyle ||
81
+ input.fontColor ||
82
+ input.textAlign ||
83
+ input.textBaseline ||
84
+ input.lineHeight !== undefined
85
+ )
86
+ if (noOp) return { ops: [], errors: ['No font changes specified'] }
87
+
88
+ if (input.fontSize !== undefined && (input.fontSize <= 0 || !Number.isFinite(input.fontSize))) {
89
+ return { ops: [], errors: [`Invalid fontSize: ${input.fontSize}`] }
90
+ }
91
+ if (input.lineHeight !== undefined && input.lineHeight <= 0) {
92
+ return { ops: [], errors: [`Invalid lineHeight: ${input.lineHeight}`] }
93
+ }
94
+ if (input.fontColor !== undefined) {
95
+ const v = validateColor(input.fontColor)
96
+ if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid fontColor'] }
97
+ }
98
+ if (input.fontStyle !== undefined && !VALID_FONT_STYLE.has(input.fontStyle)) {
99
+ return {
100
+ ops: [],
101
+ errors: [`Unknown fontStyle: '${input.fontStyle}'. Valid: ${[...VALID_FONT_STYLE].join(' / ')}.`]
102
+ }
103
+ }
104
+ if (input.textAlign !== undefined && !VALID_TEXT_ALIGN.has(input.textAlign)) {
105
+ return {
106
+ ops: [],
107
+ errors: [`Unknown textAlign: '${input.textAlign}'. Valid: ${[...VALID_TEXT_ALIGN].join(' / ')}.`]
108
+ }
109
+ }
110
+ if (input.textBaseline !== undefined && !VALID_TEXT_BASELINE.has(input.textBaseline)) {
111
+ return {
112
+ ops: [],
113
+ errors: [`Unknown textBaseline: '${input.textBaseline}'. Valid: ${[...VALID_TEXT_BASELINE].join(' / ')}.`]
114
+ }
115
+ }
116
+
117
+ const patch: any = {}
118
+ if (input.fontFamily) patch.fontFamily = input.fontFamily
119
+ if (input.fontSize !== undefined) patch.fontSize = input.fontSize
120
+ if (input.fontWeight !== undefined) patch.fontWeight = String(input.fontWeight)
121
+ if (input.fontStyle) patch.fontStyle = input.fontStyle
122
+ if (input.fontColor !== undefined) patch.fontColor = normalizeColor(input.fontColor)
123
+ if (input.textAlign) patch.textAlign = input.textAlign
124
+ if (input.textBaseline) patch.textBaseline = input.textBaseline
125
+ if (input.lineHeight !== undefined) patch.lineHeight = input.lineHeight
126
+
127
+ return {
128
+ ops: refids.map(refid => ({ op: 'modify', refid, patch })),
129
+ errors: []
130
+ }
131
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Tier 1 styling tool 단위 테스트 — setStroke / setText / setFont /
3
+ * setShadow / setOpacity / setVisible / setMaterial / setShadowProperties /
4
+ * clearStyle.
5
+ */
6
+ import { buildSetStrokeOps } from './stroke-tools'
7
+ import { buildSetTextOps, buildSetFontOps } from './text-tools'
8
+ import { buildSetShadowOps, buildSetOpacityOps, buildSetVisibleOps } from './effect-tools'
9
+ import { buildSetMaterialOps, buildSetShadowPropertiesOps } from './material-tools'
10
+ import { buildClearStyleOps } from './clear-tools'
11
+
12
+ describe('setStroke — top-level flat fields (things-scene 실제 모델)', () => {
13
+ test('color → strokeStyle 문자열, width → lineWidth top-level', () => {
14
+ const r = buildSetStrokeOps({ refids: [1], strokeStyle: 'red', lineWidth: 2 })
15
+ expect(r.ops[0].patch).toEqual({
16
+ strokeStyle: '#ff0000',
17
+ lineWidth: 2
18
+ })
19
+ })
20
+
21
+ test('dash preset alias → things-scene 이름 매핑', () => {
22
+ expect((buildSetStrokeOps({ refids: [1], dash: 'dashed' }).ops[0] as any).patch.lineDash).toBe('dash')
23
+ expect((buildSetStrokeOps({ refids: [1], dash: 'dotted' }).ops[0] as any).patch.lineDash).toBe('square-dot')
24
+ expect((buildSetStrokeOps({ refids: [1], dash: 'dashdot' }).ops[0] as any).patch.lineDash).toBe('dash-dot')
25
+ })
26
+
27
+ test('things-scene 본래 preset 이름도 그대로 받음', () => {
28
+ expect((buildSetStrokeOps({ refids: [1], dash: 'long-dash' }).ops[0] as any).patch.lineDash).toBe('long-dash')
29
+ expect((buildSetStrokeOps({ refids: [1], dash: 'round-dot' }).ops[0] as any).patch.lineDash).toBe('round-dot')
30
+ expect((buildSetStrokeOps({ refids: [1], dash: 'solid' }).ops[0] as any).patch.lineDash).toBe('solid')
31
+ })
32
+
33
+ test('알 수 없는 dash → 에러 + 옵션 안내', () => {
34
+ const r = buildSetStrokeOps({ refids: [1], dash: 'wavy' })
35
+ expect(r.ops).toEqual([])
36
+ expect(r.errors[0]).toMatch(/Unknown dash/)
37
+ expect(r.errors[0]).toMatch(/long-dash/)
38
+ })
39
+
40
+ test('enabled=false → lineWidth 0', () => {
41
+ const r = buildSetStrokeOps({ refids: [1], enabled: false, strokeStyle: 'red' })
42
+ expect((r.ops[0] as any).patch.lineWidth).toBe(0)
43
+ })
44
+
45
+ test('lineCap / lineJoin top-level', () => {
46
+ const r = buildSetStrokeOps({
47
+ refids: [1],
48
+ lineCap: 'round',
49
+ lineJoin: 'bevel'
50
+ })
51
+ expect((r.ops[0] as any).patch).toMatchObject({
52
+ lineCap: 'round',
53
+ lineJoin: 'bevel'
54
+ })
55
+ // strokeStyle 객체 구조 절대 출력 안 됨
56
+ expect((r.ops[0] as any).patch.strokeStyle).toBeUndefined()
57
+ })
58
+
59
+ test('multi-target', () => {
60
+ const r = buildSetStrokeOps({ refids: [1, 2, 3], strokeStyle: 'navy' })
61
+ expect(r.ops).toHaveLength(3)
62
+ expect(r.ops.every(o => (o as any).patch.strokeStyle === '#000080')).toBe(true)
63
+ })
64
+
65
+ test('전체 케이스 — 모든 필드 top-level', () => {
66
+ const r = buildSetStrokeOps({
67
+ refids: [15],
68
+ strokeStyle: '#ffffff',
69
+ lineWidth: 4,
70
+ dash: 'dashed',
71
+ lineCap: 'round',
72
+ lineJoin: 'round'
73
+ })
74
+ expect(r.ops[0].patch).toEqual({
75
+ strokeStyle: '#ffffff',
76
+ lineWidth: 4,
77
+ lineDash: 'dash',
78
+ lineCap: 'round',
79
+ lineJoin: 'round'
80
+ })
81
+ })
82
+ })
83
+
84
+ describe('setText', () => {
85
+ test('text 변경', () => {
86
+ const r = buildSetTextOps({ refids: [1, 2], text: 'OPERATO' })
87
+ expect(r.ops).toHaveLength(2)
88
+ expect(r.ops[0].patch).toEqual({ text: 'OPERATO' })
89
+ })
90
+
91
+ test('text 비-string → 에러', () => {
92
+ const r = buildSetTextOps({ refids: [1], text: 123 as any })
93
+ expect(r.ops).toEqual([])
94
+ })
95
+ })
96
+
97
+ describe('setFont', () => {
98
+ test('size + weight + color', () => {
99
+ const r = buildSetFontOps({
100
+ refids: [1],
101
+ fontSize: 24,
102
+ fontWeight: 'bold',
103
+ fontColor: 'navy'
104
+ })
105
+ expect(r.ops[0].patch).toEqual({
106
+ fontSize: 24,
107
+ fontWeight: 'bold',
108
+ fontColor: '#000080'
109
+ })
110
+ })
111
+
112
+ test('숫자 weight → string 변환', () => {
113
+ const r = buildSetFontOps({ refids: [1], fontWeight: 700 })
114
+ expect((r.ops[0] as any).patch.fontWeight).toBe('700')
115
+ })
116
+
117
+ test('잘못된 fontSize → 에러', () => {
118
+ const r = buildSetFontOps({ refids: [1], fontSize: -5 })
119
+ expect(r.ops).toEqual([])
120
+ expect(r.errors[0]).toMatch(/Invalid fontSize/)
121
+ })
122
+
123
+ test('잘못된 fontColor → 에러', () => {
124
+ const r = buildSetFontOps({ refids: [1], fontColor: 'invalid-color' })
125
+ expect(r.ops).toEqual([])
126
+ expect(r.errors[0]).toMatch(/Invalid color/i)
127
+ })
128
+
129
+ test('textAlign / textBaseline', () => {
130
+ const r = buildSetFontOps({
131
+ refids: [1],
132
+ textAlign: 'center',
133
+ textBaseline: 'middle'
134
+ })
135
+ expect((r.ops[0] as any).patch).toMatchObject({
136
+ textAlign: 'center',
137
+ textBaseline: 'middle'
138
+ })
139
+ })
140
+ })
141
+
142
+ describe('setShadow — canonical fields (CSS / Canvas 표준)', () => {
143
+ test('canonical fields → state.shadow.{offsetX,offsetY,blur,color}', () => {
144
+ const r = buildSetShadowOps({
145
+ refids: [1],
146
+ color: 'rgba(0,0,0,0.3)',
147
+ blur: 8,
148
+ offsetX: 2,
149
+ offsetY: 4
150
+ })
151
+ expect((r.ops[0] as any).patch.shadow).toEqual({
152
+ color: '#000000',
153
+ blur: 8,
154
+ offsetX: 2,
155
+ offsetY: 4
156
+ })
157
+ // 옛 stale 이름은 출력 안 됨 — backwards-compat 은 things-scene drawer 의 read-side 만
158
+ expect((r.ops[0] as any).patch.shadow.left).toBeUndefined()
159
+ expect((r.ops[0] as any).patch.shadow.top).toBeUndefined()
160
+ expect((r.ops[0] as any).patch.shadow.blurSize).toBeUndefined()
161
+ })
162
+
163
+ test('enabled=false → blur 0 + transparent', () => {
164
+ const r = buildSetShadowOps({ refids: [1], enabled: false })
165
+ expect((r.ops[0] as any).patch.shadow).toEqual({
166
+ blur: 0,
167
+ color: 'transparent'
168
+ })
169
+ })
170
+
171
+ test('잘못된 blur → 에러', () => {
172
+ const r = buildSetShadowOps({ refids: [1], blur: -1 })
173
+ expect(r.ops).toEqual([])
174
+ expect(r.errors[0]).toMatch(/Invalid blur/)
175
+ })
176
+ })
177
+
178
+ describe('setOpacity', () => {
179
+ test('alpha 설정', () => {
180
+ const r = buildSetOpacityOps({ refids: [1, 2], alpha: 0.5 })
181
+ expect(r.ops).toHaveLength(2)
182
+ expect(r.ops[0].patch).toEqual({ alpha: 0.5 })
183
+ })
184
+
185
+ test('범위 밖 → 에러', () => {
186
+ const r = buildSetOpacityOps({ refids: [1], alpha: 1.5 })
187
+ expect(r.ops).toEqual([])
188
+ expect(r.errors[0]).toMatch(/alpha must be 0..1/)
189
+ })
190
+ })
191
+
192
+ describe('setVisible', () => {
193
+ test('hidden 토글', () => {
194
+ const r1 = buildSetVisibleOps({ refids: [1], visible: false })
195
+ expect(r1.ops[0].patch).toEqual({ hidden: true })
196
+ const r2 = buildSetVisibleOps({ refids: [1], visible: true })
197
+ expect(r2.ops[0].patch).toEqual({ hidden: false })
198
+ })
199
+
200
+ test('visible 비-boolean → 에러', () => {
201
+ const r = buildSetVisibleOps({ refids: [1], visible: 'yes' as any })
202
+ expect(r.ops).toEqual([])
203
+ })
204
+ })
205
+
206
+ describe('setMaterial — 3D PBR', () => {
207
+ test('preset 만', () => {
208
+ const r = buildSetMaterialOps({ refids: [1], preset: 'metal' })
209
+ expect(r.ops[0].patch).toEqual({ material3d: { preset: 'metal' } })
210
+ })
211
+
212
+ test('preset + override', () => {
213
+ const r = buildSetMaterialOps({
214
+ refids: [1],
215
+ preset: 'glass',
216
+ opacity: 0.4
217
+ })
218
+ expect((r.ops[0] as any).patch.material3d).toEqual({
219
+ preset: 'glass',
220
+ opacity: 0.4
221
+ })
222
+ })
223
+
224
+ test('emissive 컬러 + intensity', () => {
225
+ const r = buildSetMaterialOps({
226
+ refids: [1],
227
+ emissive: 'amber',
228
+ emissiveIntensity: 2
229
+ })
230
+ expect((r.ops[0] as any).patch.material3d).toEqual({
231
+ emissive: '#f59e0b',
232
+ emissiveIntensity: 2
233
+ })
234
+ })
235
+
236
+ test('알 수 없는 preset → 에러', () => {
237
+ const r = buildSetMaterialOps({ refids: [1], preset: 'foobar' as any })
238
+ expect(r.ops).toEqual([])
239
+ expect(r.errors[0]).toMatch(/Unknown preset/)
240
+ })
241
+
242
+ test('metalness 범위 밖 → 에러', () => {
243
+ const r = buildSetMaterialOps({ refids: [1], metalness: 2 })
244
+ expect(r.ops).toEqual([])
245
+ })
246
+
247
+ test('side 명시', () => {
248
+ const r = buildSetMaterialOps({ refids: [1], side: 'double' })
249
+ expect((r.ops[0] as any).patch.material3d.side).toBe('double')
250
+ })
251
+ })
252
+
253
+ describe('setShadowProperties — 3D 그림자 동작', () => {
254
+ test('castShadow + receiveShadow', () => {
255
+ const r = buildSetShadowPropertiesOps({
256
+ refids: [1],
257
+ castShadow: true,
258
+ receiveShadow: true
259
+ })
260
+ expect((r.ops[0] as any).patch.material3d).toEqual({
261
+ castShadow: true,
262
+ receiveShadow: true
263
+ })
264
+ })
265
+
266
+ test('아무것도 없으면 에러', () => {
267
+ const r = buildSetShadowPropertiesOps({ refids: [1] })
268
+ expect(r.ops).toEqual([])
269
+ })
270
+ })
271
+
272
+ describe('clearStyle', () => {
273
+ test('fill 만', () => {
274
+ const r = buildClearStyleOps({ refids: [1], aspects: ['fill'] })
275
+ expect(r.ops[0].patch).toEqual({ fillStyle: { type: 'none' } })
276
+ })
277
+
278
+ test('여러 aspect 합쳐서 한 patch', () => {
279
+ const r = buildClearStyleOps({
280
+ refids: [1],
281
+ aspects: ['fill', 'stroke', 'shadow']
282
+ })
283
+ const patch = (r.ops[0] as any).patch
284
+ expect(patch.fillStyle).toBeDefined()
285
+ // stroke clear = lineWidth 0 (색은 유지) — clear-tools.ts:22 참조
286
+ expect(patch.lineWidth).toBe(0)
287
+ expect(patch.shadow).toBeDefined()
288
+ })
289
+
290
+ test('알 수 없는 aspect → 에러', () => {
291
+ const r = buildClearStyleOps({ refids: [1], aspects: ['blink' as any] })
292
+ expect(r.ops).toEqual([])
293
+ expect(r.errors[0]).toMatch(/Unknown aspect/)
294
+ })
295
+
296
+ test('opacity reset → alpha 1', () => {
297
+ const r = buildClearStyleOps({ refids: [1], aspects: ['opacity'] })
298
+ expect((r.ops[0] as any).patch.alpha).toBe(1)
299
+ })
300
+ })
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 구독 도메인 인증 게이트 — 단위 테스트 (보안 핵심: 도메인 격리).
3
+ * board-patch / chat-message 양쪽 spine 이 공유하는 게이트.
4
+ *
5
+ * 회귀 방지: process.superUserGranted 는 async 다. 동기 `!process.superUserGranted(...)` 로
6
+ * 평가하면 `!Promise` = 항상 false → membership throw 가 죽는다(fail-open). 게이트는 반드시
7
+ * await 로 평가해야 한다. 아래 superUser 분기 테스트가 그 회귀를 박는다.
8
+ */
9
+ import { assertDomainSubscribeAllowed } from './subscribe-auth'
10
+
11
+ const never = async () => false // superUser 아님 (async — 실제 process.superUserGranted 와 동일)
12
+ const always = async () => true // superUser (async)
13
+
14
+ const domain = { id: 'd1', subdomain: 'acme' }
15
+ const userInDomain = { email: 'a@x.io', domains: [{ subdomain: 'acme' }, { subdomain: 'other' }] }
16
+ const userOutDomain = { email: 'b@x.io', domains: [{ subdomain: 'other' }] }
17
+
18
+ describe('assertDomainSubscribeAllowed (도메인 격리 게이트)', () => {
19
+ it('domain 없으면 → reject (domain required)', async () => {
20
+ await expect(assertDomainSubscribeAllowed(undefined, userInDomain, never)).rejects.toThrow('domain required')
21
+ await expect(assertDomainSubscribeAllowed(null, userInDomain, never)).rejects.toThrow('domain required')
22
+ })
23
+
24
+ it('user 가 해당 subdomain 소속 → 통과 (superUser 검사 불필요)', async () => {
25
+ await expect(assertDomainSubscribeAllowed(domain, userInDomain, never)).resolves.toBeUndefined()
26
+ })
27
+
28
+ it('user 가 미소속 + superUser 아님 → reject (격리 — 핵심)', async () => {
29
+ await expect(assertDomainSubscribeAllowed(domain, userOutDomain, never)).rejects.toThrow(
30
+ 'domain(acme) is not working for user(b@x.io).'
31
+ )
32
+ })
33
+
34
+ it('user 가 미소속이어도 superUser(async resolve true) 면 → 통과 (await 회귀 방지)', async () => {
35
+ await expect(assertDomainSubscribeAllowed(domain, userOutDomain, always)).resolves.toBeUndefined()
36
+ })
37
+
38
+ it('user.domains 없음 + superUser 아님 → reject', async () => {
39
+ await expect(assertDomainSubscribeAllowed(domain, { email: 'c@x.io' }, never)).rejects.toThrow(
40
+ 'domain(acme) is not working for user(c@x.io).'
41
+ )
42
+ })
43
+
44
+ it('user.domains 없음 + superUser → 통과', async () => {
45
+ await expect(assertDomainSubscribeAllowed(domain, { email: 'c@x.io' }, always)).resolves.toBeUndefined()
46
+ })
47
+
48
+ it('isSuperUser 는 domain+user 를 받아 호출되고 await 된다', async () => {
49
+ const calls: any[] = []
50
+ const spy = async (d: any, u: any) => {
51
+ calls.push([d, u])
52
+ return true
53
+ }
54
+ await assertDomainSubscribeAllowed(domain, userOutDomain, spy)
55
+ expect(calls).toEqual([[domain, userOutDomain]])
56
+ })
57
+ })
@@ -0,0 +1,34 @@
1
+ /*
2
+ * 구독 도메인 인증 게이트 — 순수 로직 (데코레이터·pubsub 없음 → 테스트 가능).
3
+ *
4
+ * 실시간 구독(@Subscription)은 같은 도메인 구성원만 허용해야 한다(도메인 격리 = 보안 핵심).
5
+ * 이 게이트는 board-patch / chat-message 양쪽 spine 의 subscribe 클로저가 공유한다
6
+ * (reuse — fork 금지). superUserGranted 는 process 전역 의존이라 주입받아 순수성 유지.
7
+ */
8
+
9
+ /**
10
+ * 구독 허용 여부 단언. 불가 시 throw (graphql-ws 가 구독 거부).
11
+ * - domain 없음 → 거부.
12
+ * - user 가 해당 subdomain 의 도메인 소속이 아니고 superUser 도 아니면 → 거부.
13
+ *
14
+ * 주의(버그 수정): process.superUserGranted 는 async(Promise<boolean>) 다. 기존 양쪽 spine 은
15
+ * `!process.superUserGranted(...)` 로 동기 평가해 `!Promise` = 항상 false → membership throw 가
16
+ * 발동하지 않는 fail-open 상태였다. 여기서는 await 로 제대로 평가한다 → subscribe 는 async.
17
+ *
18
+ * @param domain context.state.domain
19
+ * @param user context.state.user (domains[] / email 사용)
20
+ * @param isSuperUser (domain, user) → boolean | Promise<boolean> (process.superUserGranted 주입)
21
+ */
22
+ export async function assertDomainSubscribeAllowed(
23
+ domain: any,
24
+ user: any,
25
+ isSuperUser: (domain: any, user: any) => boolean | Promise<boolean>
26
+ ): Promise<void> {
27
+ if (!domain) {
28
+ throw new Error('domain required')
29
+ }
30
+ const subdomain = domain?.subdomain
31
+ if (!user?.domains?.find((d: any) => d.subdomain === subdomain) && !(await isSuperUser(domain, user))) {
32
+ throw new Error(`domain(${subdomain}) is not working for user(${user?.email}).`)
33
+ }
34
+ }