@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,245 @@
1
+ /**
2
+ * stroke-tools — buildSetStrokeOps 입력 → BoardEditOp[] 변환 검증.
3
+ *
4
+ * things-scene canonical fields (모두 top-level): strokeStyle (string color), lineWidth,
5
+ * lineDash (preset 이름 string), lineCap, lineJoin. tool 파라미터 이름이 engine 필드와
6
+ * 1:1 (rename 없음). dash 만 CSS-친화 alias (dashed/dotted/dashdot) → preset 매핑.
7
+ */
8
+ import { buildSetStrokeOps } from './stroke-tools'
9
+
10
+ describe('buildSetStrokeOps — 입력 검증', () => {
11
+ test('refids 비어있으면 에러', () => {
12
+ const r = buildSetStrokeOps({ refids: [], strokeStyle: 'red' })
13
+ expect(r.ops).toEqual([])
14
+ expect(r.errors[0]).toMatch(/non-empty array/)
15
+ })
16
+
17
+ test('비-숫자 refids 필터링', () => {
18
+ const r = buildSetStrokeOps({
19
+ refids: [1, 'abc' as any, NaN, 2],
20
+ strokeStyle: 'red'
21
+ })
22
+ expect(r.ops).toHaveLength(2)
23
+ expect(r.ops[0]).toMatchObject({ refid: 1, patch: { strokeStyle: '#ff0000' } })
24
+ expect(r.ops[1]).toMatchObject({ refid: 2, patch: { strokeStyle: '#ff0000' } })
25
+ })
26
+
27
+ test('아무 인자도 없으면 에러 — 의도 모호', () => {
28
+ const r = buildSetStrokeOps({ refids: [1] })
29
+ expect(r.ops).toEqual([])
30
+ expect(r.errors[0]).toMatch(/No stroke changes/)
31
+ })
32
+
33
+ test('잘못된 색상 → 에러', () => {
34
+ const r = buildSetStrokeOps({ refids: [1], strokeStyle: 'reddish' })
35
+ expect(r.ops).toEqual([])
36
+ expect(r.errors[0]).toMatch(/Invalid/i)
37
+ })
38
+
39
+ test('lineWidth 음수 → 에러', () => {
40
+ const r = buildSetStrokeOps({ refids: [1], lineWidth: -1 })
41
+ expect(r.ops).toEqual([])
42
+ expect(r.errors[0]).toMatch(/Invalid lineWidth/)
43
+ })
44
+
45
+ test('lineWidth 0 은 허용 — 명시적 hide 의도와 구분되는 의도된 두께 0', () => {
46
+ const r = buildSetStrokeOps({ refids: [1], lineWidth: 0 })
47
+ expect(r.ops).toHaveLength(1)
48
+ expect(r.ops[0].patch).toEqual({ lineWidth: 0 })
49
+ })
50
+
51
+ test('lineWidth Infinity → 에러', () => {
52
+ const r = buildSetStrokeOps({ refids: [1], lineWidth: Infinity })
53
+ expect(r.ops).toEqual([])
54
+ expect(r.errors[0]).toMatch(/Invalid lineWidth/)
55
+ })
56
+ })
57
+
58
+ describe('buildSetStrokeOps — strokeStyle (canonical, no rename)', () => {
59
+ test('hex → 정규화된 hex 문자열', () => {
60
+ const r = buildSetStrokeOps({ refids: [1], strokeStyle: '#ff0000' })
61
+ expect(r.ops[0].patch).toEqual({ strokeStyle: '#ff0000' })
62
+ })
63
+
64
+ test('CSS named → hex', () => {
65
+ const r = buildSetStrokeOps({ refids: [1], strokeStyle: 'red' })
66
+ expect(r.ops[0].patch).toEqual({ strokeStyle: '#ff0000' })
67
+ })
68
+
69
+ test('semantic alias (warm-red)', () => {
70
+ const r = buildSetStrokeOps({ refids: [1], strokeStyle: 'warm-red' })
71
+ expect(r.ops[0].patch).toEqual({ strokeStyle: '#e74c3c' })
72
+ })
73
+ })
74
+
75
+ describe('buildSetStrokeOps — lineWidth (canonical, no rename)', () => {
76
+ test('lineWidth 만 변경', () => {
77
+ const r = buildSetStrokeOps({ refids: [1], lineWidth: 3 })
78
+ expect(r.ops).toHaveLength(1)
79
+ expect(r.ops[0].patch).toEqual({ lineWidth: 3 })
80
+ })
81
+
82
+ test('strokeStyle + lineWidth 동시', () => {
83
+ const r = buildSetStrokeOps({ refids: [1], strokeStyle: 'navy', lineWidth: 2 })
84
+ expect(r.ops[0].patch).toEqual({ strokeStyle: '#000080', lineWidth: 2 })
85
+ })
86
+ })
87
+
88
+ describe('buildSetStrokeOps — dash 정규화 (alias → preset)', () => {
89
+ test('AI 친화 alias dashed → dash', () => {
90
+ const r = buildSetStrokeOps({ refids: [1], dash: 'dashed' })
91
+ expect(r.ops[0].patch).toEqual({ lineDash: 'dash' })
92
+ })
93
+
94
+ test('AI 친화 alias dotted → square-dot', () => {
95
+ const r = buildSetStrokeOps({ refids: [1], dash: 'dotted' })
96
+ expect(r.ops[0].patch).toEqual({ lineDash: 'square-dot' })
97
+ })
98
+
99
+ test('AI 친화 alias dashdot → dash-dot', () => {
100
+ const r = buildSetStrokeOps({ refids: [1], dash: 'dashdot' })
101
+ expect(r.ops[0].patch).toEqual({ lineDash: 'dash-dot' })
102
+ })
103
+
104
+ test('things-scene preset 그대로 통과 (solid)', () => {
105
+ const r = buildSetStrokeOps({ refids: [1], dash: 'solid' })
106
+ expect(r.ops[0].patch).toEqual({ lineDash: 'solid' })
107
+ })
108
+
109
+ test('long-dash-dot-dot 같은 긴 preset', () => {
110
+ const r = buildSetStrokeOps({ refids: [1], dash: 'long-dash-dot-dot' })
111
+ expect(r.ops[0].patch).toEqual({ lineDash: 'long-dash-dot-dot' })
112
+ })
113
+
114
+ test('round-dot preset', () => {
115
+ const r = buildSetStrokeOps({ refids: [1], dash: 'round-dot' })
116
+ expect(r.ops[0].patch).toEqual({ lineDash: 'round-dot' })
117
+ })
118
+
119
+ test('알 수 없는 dash → 에러 + 유효 목록 제시', () => {
120
+ const r = buildSetStrokeOps({ refids: [1], dash: 'wave' as any })
121
+ expect(r.ops).toEqual([])
122
+ expect(r.errors[0]).toMatch(/Unknown dash/)
123
+ expect(r.errors[0]).toMatch(/solid/)
124
+ })
125
+ })
126
+
127
+ describe('buildSetStrokeOps — lineCap / lineJoin', () => {
128
+ test('lineCap=round', () => {
129
+ const r = buildSetStrokeOps({ refids: [1], lineCap: 'round' })
130
+ expect(r.ops[0].patch).toEqual({ lineCap: 'round' })
131
+ })
132
+
133
+ test('lineJoin=miter', () => {
134
+ const r = buildSetStrokeOps({ refids: [1], lineJoin: 'miter' })
135
+ expect(r.ops[0].patch).toEqual({ lineJoin: 'miter' })
136
+ })
137
+
138
+ test('cap + join + lineWidth + strokeStyle 조합', () => {
139
+ const r = buildSetStrokeOps({
140
+ refids: [1],
141
+ strokeStyle: '#333',
142
+ lineWidth: 2,
143
+ lineCap: 'round',
144
+ lineJoin: 'bevel'
145
+ })
146
+ expect(r.ops[0].patch).toEqual({
147
+ strokeStyle: '#333333',
148
+ lineWidth: 2,
149
+ lineCap: 'round',
150
+ lineJoin: 'bevel'
151
+ })
152
+ })
153
+
154
+ test('잘못된 lineCap → 에러 + 유효 목록 제시', () => {
155
+ const r = buildSetStrokeOps({ refids: [1], lineCap: 'flat' as any })
156
+ expect(r.ops).toEqual([])
157
+ expect(r.errors[0]).toMatch(/Unknown lineCap.*flat/)
158
+ expect(r.errors[0]).toMatch(/butt/)
159
+ })
160
+
161
+ test('잘못된 lineJoin → 에러 + 유효 목록 제시', () => {
162
+ const r = buildSetStrokeOps({ refids: [1], lineJoin: 'sharp' as any })
163
+ expect(r.ops).toEqual([])
164
+ expect(r.errors[0]).toMatch(/Unknown lineJoin.*sharp/)
165
+ expect(r.errors[0]).toMatch(/miter/)
166
+ })
167
+ })
168
+
169
+ describe('buildSetStrokeOps — enabled=false (synthetic helper, stroke 제거)', () => {
170
+ test('enabled=false → lineWidth 0 (drawer/stroke.ts 가 안 그림)', () => {
171
+ const r = buildSetStrokeOps({ refids: [1], enabled: false })
172
+ expect(r.ops).toHaveLength(1)
173
+ expect(r.ops[0].patch).toEqual({ lineWidth: 0 })
174
+ })
175
+
176
+ test('enabled=false 가 명시 lineWidth 보다 우선', () => {
177
+ // 모순 입력: 사용자가 enabled=false 라고 했으면 lineWidth 무시하고 0
178
+ const r = buildSetStrokeOps({ refids: [1], enabled: false, lineWidth: 5 })
179
+ expect(r.ops[0].patch).toEqual({ lineWidth: 0 })
180
+ })
181
+
182
+ test('enabled=true 단독 — 다른 필드 없으면 patch 비어있는 op 안 만듦', () => {
183
+ // 현재 구현: enabled=true 단독은 noOp 가 아니지만 patch 가 비어 있음
184
+ const r = buildSetStrokeOps({ refids: [1], enabled: true })
185
+ expect(r.ops).toHaveLength(1)
186
+ expect(r.ops[0].patch).toEqual({})
187
+ })
188
+ })
189
+
190
+ describe('buildSetStrokeOps — multi-target', () => {
191
+ test('동일 patch 가 여러 refid 에 분배', () => {
192
+ const r = buildSetStrokeOps({
193
+ refids: [10, 20, 30],
194
+ strokeStyle: 'red',
195
+ lineWidth: 2
196
+ })
197
+ expect(r.ops).toHaveLength(3)
198
+ const expectedPatch = { strokeStyle: '#ff0000', lineWidth: 2 }
199
+ expect(r.ops[0]).toEqual({ op: 'modify', refid: 10, patch: expectedPatch })
200
+ expect(r.ops[1]).toEqual({ op: 'modify', refid: 20, patch: expectedPatch })
201
+ expect(r.ops[2]).toEqual({ op: 'modify', refid: 30, patch: expectedPatch })
202
+ })
203
+
204
+ test('5 컴포넌트 동일 dashed 라인', () => {
205
+ const r = buildSetStrokeOps({
206
+ refids: [1, 2, 3, 4, 5],
207
+ strokeStyle: '#888',
208
+ lineWidth: 1,
209
+ dash: 'dashed'
210
+ })
211
+ expect(r.ops).toHaveLength(5)
212
+ expect(r.ops.every(o => (o as any).patch.lineDash === 'dash')).toBe(true)
213
+ expect(r.ops.every(o => (o as any).patch.strokeStyle === '#888888')).toBe(true)
214
+ })
215
+ })
216
+
217
+ describe('buildSetStrokeOps — 시나리오', () => {
218
+ test('테두리 강조 — 흰색 두께 3', () => {
219
+ const r = buildSetStrokeOps({ refids: [42], strokeStyle: 'white', lineWidth: 3 })
220
+ expect(r.ops[0].patch).toEqual({ strokeStyle: '#ffffff', lineWidth: 3 })
221
+ })
222
+
223
+ test('점선 가이드 — 회색 1px dashed', () => {
224
+ const r = buildSetStrokeOps({
225
+ refids: [42],
226
+ strokeStyle: '#aaa',
227
+ lineWidth: 1,
228
+ dash: 'dashed'
229
+ })
230
+ expect(r.ops[0].patch).toEqual({
231
+ strokeStyle: '#aaaaaa',
232
+ lineWidth: 1,
233
+ lineDash: 'dash'
234
+ })
235
+ })
236
+
237
+ test('테마 적용 — strokeStyle 만 (테마 정책 준수)', () => {
238
+ // 테마 요청 시 색상 만 변경, lineWidth/dash 는 건드리지 않음
239
+ const r = buildSetStrokeOps({ refids: [1, 2], strokeStyle: 'navy' })
240
+ expect(r.ops).toHaveLength(2)
241
+ expect(r.ops[0].patch).toEqual({ strokeStyle: '#000080' })
242
+ expect((r.ops[0] as any).patch.lineWidth).toBeUndefined()
243
+ expect((r.ops[0] as any).patch.lineDash).toBeUndefined()
244
+ })
245
+ })
@@ -0,0 +1,145 @@
1
+ /**
2
+ * setStroke — 컴포넌트 테두리 / 라인 스타일.
3
+ *
4
+ * **things-scene 의 실제 데이터 형식 (drawer/stroke.ts 검증):**
5
+ * - `state.strokeStyle: string` (색상 — Canvas API 의 strokeStyle 에 직접 대입되는 CSS color)
6
+ * - `state.lineWidth: number` (top-level)
7
+ * - `state.lineDash: string` (top-level — preset 이름. drawer 가 lineWidth 기반으로 배열 변환)
8
+ * valid: 'solid' | 'round-dot' | 'square-dot' | 'dash' | 'dash-dot' | 'long-dash' | 'long-dash-dot' | 'long-dash-dot-dot'
9
+ * - `state.lineCap: 'butt' | 'round' | 'square'` (top-level)
10
+ * - `state.lineJoin: 'miter' | 'round' | 'bevel'` (top-level)
11
+ *
12
+ * 인터페이스의 `StrokeStyle` 객체 구조는 stale. 사용처는 모두 top-level 직접 접근.
13
+ *
14
+ * 입력 단순화 — AI 친화적 alias 도 받아 things-scene preset 으로 매핑:
15
+ * AI 'dashed' → 'dash', 'dotted' → 'square-dot', 'dashdot' → 'dash-dot'.
16
+ */
17
+ import type { BoardEditOp } from '../types'
18
+ import { normalizeColor, validateColor } from './color-utils'
19
+
20
+ export interface SetStrokeInput {
21
+ refids: number[]
22
+ /** synthetic — false 면 lineWidth=0 으로 매핑 (drawer 가 안 그림). 모델에 enabled 필드는 안 박힘. */
23
+ enabled?: boolean
24
+ /** state.strokeStyle 의 canonical 이름. CSS color string. */
25
+ strokeStyle?: string
26
+ /** state.lineWidth 의 canonical 이름. pixels. */
27
+ lineWidth?: number
28
+ /** preset 이름. AI 친화 alias 자동 매핑 — 'dashed' / 'dotted' / 'dashdot' 도 가능. */
29
+ dash?:
30
+ | 'solid'
31
+ | 'dashed'
32
+ | 'dotted'
33
+ | 'dashdot'
34
+ | 'round-dot'
35
+ | 'square-dot'
36
+ | 'dash'
37
+ | 'dash-dot'
38
+ | 'long-dash'
39
+ | 'long-dash-dot'
40
+ | 'long-dash-dot-dot'
41
+ | string
42
+ lineCap?: 'butt' | 'round' | 'square'
43
+ lineJoin?: 'miter' | 'round' | 'bevel'
44
+ }
45
+
46
+ export interface SetStrokeResult {
47
+ ops: BoardEditOp[]
48
+ errors: string[]
49
+ }
50
+
51
+ /** AI 친화 alias → things-scene 의 실제 preset 이름. */
52
+ const DASH_ALIASES: Record<string, string> = {
53
+ dashed: 'dash',
54
+ dotted: 'square-dot',
55
+ dashdot: 'dash-dot'
56
+ }
57
+
58
+ const VALID_DASH = new Set([
59
+ 'solid',
60
+ 'round-dot',
61
+ 'square-dot',
62
+ 'dash',
63
+ 'dash-dot',
64
+ 'long-dash',
65
+ 'long-dash-dot',
66
+ 'long-dash-dot-dot'
67
+ ])
68
+
69
+ // Canvas CanvasRenderingContext2D 가 허용하는 lineCap / lineJoin 만.
70
+ // 잘못된 값은 Canvas 가 silent ignore → AI 가 'flat' / 'cap' 등을 보내도
71
+ // 효과 없음. 명시 검증으로 ops 미실행 + 명확한 에러.
72
+ const VALID_LINE_CAP = new Set(['butt', 'round', 'square'])
73
+ const VALID_LINE_JOIN = new Set(['miter', 'round', 'bevel'])
74
+
75
+ export function buildSetStrokeOps(input: SetStrokeInput): SetStrokeResult {
76
+ if (!Array.isArray(input.refids) || input.refids.length === 0) {
77
+ return { ops: [], errors: ['refids must be a non-empty array of numbers'] }
78
+ }
79
+ const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
80
+ if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
81
+
82
+ const noOp =
83
+ input.enabled === undefined &&
84
+ input.strokeStyle === undefined &&
85
+ input.lineWidth === undefined &&
86
+ input.dash === undefined &&
87
+ input.lineCap === undefined &&
88
+ input.lineJoin === undefined
89
+ if (noOp) {
90
+ return { ops: [], errors: ['No stroke changes specified'] }
91
+ }
92
+
93
+ // 검증
94
+ if (input.strokeStyle !== undefined) {
95
+ const v = validateColor(input.strokeStyle)
96
+ if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid strokeStyle'] }
97
+ }
98
+ if (input.lineWidth !== undefined && (input.lineWidth < 0 || !Number.isFinite(input.lineWidth))) {
99
+ return { ops: [], errors: [`Invalid lineWidth: ${input.lineWidth}`] }
100
+ }
101
+ if (input.lineCap !== undefined && !VALID_LINE_CAP.has(input.lineCap)) {
102
+ return {
103
+ ops: [],
104
+ errors: [`Unknown lineCap: '${input.lineCap}'. Valid: ${[...VALID_LINE_CAP].join(' / ')}.`]
105
+ }
106
+ }
107
+ if (input.lineJoin !== undefined && !VALID_LINE_JOIN.has(input.lineJoin)) {
108
+ return {
109
+ ops: [],
110
+ errors: [`Unknown lineJoin: '${input.lineJoin}'. Valid: ${[...VALID_LINE_JOIN].join(' / ')}.`]
111
+ }
112
+ }
113
+
114
+ // dash 정규화 — alias → 실제 preset → 검증
115
+ let dashValue: string | undefined
116
+ if (input.dash !== undefined) {
117
+ const raw = String(input.dash)
118
+ const resolved = DASH_ALIASES[raw] ?? raw
119
+ if (!VALID_DASH.has(resolved)) {
120
+ return {
121
+ ops: [],
122
+ errors: [
123
+ `Unknown dash: '${input.dash}'. Valid: ${[...VALID_DASH].join(' / ')} (or alias: dashed/dotted/dashdot).`
124
+ ]
125
+ }
126
+ }
127
+ dashValue = resolved
128
+ }
129
+
130
+ // patch 구성 — canonical 이름 그대로. 매핑 X. (color 만 normalizeColor 통과 — 입력 정규화는 mapping 이 아닌 처리)
131
+ const patch: any = {}
132
+ if (input.strokeStyle !== undefined) patch.strokeStyle = normalizeColor(input.strokeStyle)
133
+ if (input.lineWidth !== undefined) patch.lineWidth = input.lineWidth
134
+ if (dashValue !== undefined) patch.lineDash = dashValue
135
+ if (input.lineCap !== undefined) patch.lineCap = input.lineCap
136
+ if (input.lineJoin !== undefined) patch.lineJoin = input.lineJoin
137
+
138
+ // synthetic enabled — false 면 lineWidth 0 으로 변환 (drawer 가 안 그림 — drawer/stroke.ts:9)
139
+ if (input.enabled === false) {
140
+ patch.lineWidth = 0
141
+ }
142
+
143
+ const ops: BoardEditOp[] = refids.map(refid => ({ op: 'modify', refid, patch }))
144
+ return { ops, errors: [] }
145
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * text-tools — buildSetTextOps + buildSetFontOps.
3
+ *
4
+ * setText: state.text (콘텐츠 변경)
5
+ * setFont: state.fontFamily / fontSize / fontWeight / fontStyle / fontColor /
6
+ * textAlign / textBaseline / lineHeight (시각 스타일 변경)
7
+ *
8
+ * 두 tool 분리 — 콘텐츠 vs 시각 스타일 의도가 다름.
9
+ */
10
+ import { buildSetTextOps, buildSetFontOps } from './text-tools'
11
+
12
+ describe('buildSetTextOps — 입력 검증', () => {
13
+ test('refids 비어있으면 에러', () => {
14
+ const r = buildSetTextOps({ refids: [], text: 'hi' })
15
+ expect(r.ops).toEqual([])
16
+ expect(r.errors[0]).toMatch(/non-empty array/)
17
+ })
18
+
19
+ test('text 가 string 이 아니면 에러', () => {
20
+ const r = buildSetTextOps({ refids: [1], text: 42 as any })
21
+ expect(r.ops).toEqual([])
22
+ expect(r.errors[0]).toMatch(/text must be a string/)
23
+ })
24
+
25
+ test('비-숫자 refids 필터링', () => {
26
+ const r = buildSetTextOps({
27
+ refids: [1, NaN, 'x' as any, 2],
28
+ text: 'hello'
29
+ })
30
+ expect(r.ops).toHaveLength(2)
31
+ })
32
+
33
+ test('빈 문자열도 허용 (텍스트 비우기 의도)', () => {
34
+ const r = buildSetTextOps({ refids: [1], text: '' })
35
+ expect(r.ops).toHaveLength(1)
36
+ expect(r.ops[0].patch).toEqual({ text: '' })
37
+ })
38
+ })
39
+
40
+ describe('buildSetTextOps — text 매핑', () => {
41
+ test('단순 한국어', () => {
42
+ const r = buildSetTextOps({ refids: [1], text: '안녕' })
43
+ expect(r.ops[0]).toEqual({ op: 'modify', refid: 1, patch: { text: '안녕' } })
44
+ })
45
+
46
+ test('multi-target 동일 텍스트', () => {
47
+ const r = buildSetTextOps({ refids: [1, 2, 3], text: 'OK' })
48
+ expect(r.ops).toHaveLength(3)
49
+ expect(r.ops.every(o => (o as any).patch.text === 'OK')).toBe(true)
50
+ })
51
+
52
+ test('개행 / 특수문자 그대로 통과', () => {
53
+ const text = 'Line 1\nLine 2\t<특수>'
54
+ const r = buildSetTextOps({ refids: [1], text })
55
+ expect((r.ops[0] as any).patch.text).toBe(text)
56
+ })
57
+ })
58
+
59
+ describe('buildSetFontOps — 입력 검증', () => {
60
+ test('refids 비어있으면 에러', () => {
61
+ const r = buildSetFontOps({ refids: [], fontSize: 12 })
62
+ expect(r.ops).toEqual([])
63
+ expect(r.errors[0]).toMatch(/non-empty array/)
64
+ })
65
+
66
+ test('비-숫자 refids 필터링', () => {
67
+ const r = buildSetFontOps({
68
+ refids: [1, NaN, 'x' as any, 2],
69
+ fontSize: 14
70
+ })
71
+ expect(r.ops).toHaveLength(2)
72
+ })
73
+
74
+ test('아무 인자도 없으면 에러', () => {
75
+ const r = buildSetFontOps({ refids: [1] })
76
+ expect(r.ops).toEqual([])
77
+ expect(r.errors[0]).toMatch(/No font changes/)
78
+ })
79
+
80
+ test('fontSize 0 → 에러', () => {
81
+ const r = buildSetFontOps({ refids: [1], fontSize: 0 })
82
+ expect(r.ops).toEqual([])
83
+ expect(r.errors[0]).toMatch(/Invalid fontSize/)
84
+ })
85
+
86
+ test('fontSize 음수 → 에러', () => {
87
+ const r = buildSetFontOps({ refids: [1], fontSize: -1 })
88
+ expect(r.ops).toEqual([])
89
+ expect(r.errors[0]).toMatch(/Invalid fontSize/)
90
+ })
91
+
92
+ test('fontSize Infinity → 에러', () => {
93
+ const r = buildSetFontOps({ refids: [1], fontSize: Infinity })
94
+ expect(r.ops).toEqual([])
95
+ expect(r.errors[0]).toMatch(/Invalid fontSize/)
96
+ })
97
+
98
+ test('lineHeight 0 → 에러', () => {
99
+ const r = buildSetFontOps({ refids: [1], lineHeight: 0 })
100
+ expect(r.ops).toEqual([])
101
+ expect(r.errors[0]).toMatch(/Invalid lineHeight/)
102
+ })
103
+
104
+ test('잘못된 fontColor → 에러', () => {
105
+ const r = buildSetFontOps({ refids: [1], fontColor: 'invalid' })
106
+ expect(r.ops).toEqual([])
107
+ expect(r.errors[0]).toMatch(/Invalid/)
108
+ })
109
+
110
+ test('잘못된 fontStyle → 에러', () => {
111
+ const r = buildSetFontOps({ refids: [1], fontStyle: 'serif' as any })
112
+ expect(r.ops).toEqual([])
113
+ expect(r.errors[0]).toMatch(/Unknown fontStyle/)
114
+ })
115
+
116
+ test('잘못된 textAlign (Canvas 비표준 justify) → 에러', () => {
117
+ const r = buildSetFontOps({ refids: [1], textAlign: 'justify' as any })
118
+ expect(r.ops).toEqual([])
119
+ expect(r.errors[0]).toMatch(/Unknown textAlign/)
120
+ })
121
+
122
+ test('잘못된 textBaseline → 에러', () => {
123
+ const r = buildSetFontOps({ refids: [1], textBaseline: 'center' as any })
124
+ expect(r.ops).toEqual([])
125
+ expect(r.errors[0]).toMatch(/Unknown textBaseline/)
126
+ })
127
+ })
128
+
129
+ describe('buildSetFontOps — 필드 매핑', () => {
130
+ test('fontFamily 만', () => {
131
+ const r = buildSetFontOps({ refids: [1], fontFamily: 'Arial' })
132
+ expect(r.ops[0].patch).toEqual({ fontFamily: 'Arial' })
133
+ })
134
+
135
+ test('fontSize 만', () => {
136
+ const r = buildSetFontOps({ refids: [1], fontSize: 16 })
137
+ expect(r.ops[0].patch).toEqual({ fontSize: 16 })
138
+ })
139
+
140
+ test('fontWeight 숫자 → string 화', () => {
141
+ const r = buildSetFontOps({ refids: [1], fontWeight: 600 })
142
+ expect(r.ops[0].patch).toEqual({ fontWeight: '600' })
143
+ })
144
+
145
+ test("fontWeight 'bold' 그대로", () => {
146
+ const r = buildSetFontOps({ refids: [1], fontWeight: 'bold' })
147
+ expect(r.ops[0].patch).toEqual({ fontWeight: 'bold' })
148
+ })
149
+
150
+ test('fontStyle italic', () => {
151
+ const r = buildSetFontOps({ refids: [1], fontStyle: 'italic' })
152
+ expect(r.ops[0].patch).toEqual({ fontStyle: 'italic' })
153
+ })
154
+
155
+ test('fontColor 정규화 (named → hex)', () => {
156
+ const r = buildSetFontOps({ refids: [1], fontColor: 'red' })
157
+ expect(r.ops[0].patch).toEqual({ fontColor: '#ff0000' })
158
+ })
159
+
160
+ test('fontColor semantic alias', () => {
161
+ const r = buildSetFontOps({ refids: [1], fontColor: 'warm-red' })
162
+ expect(r.ops[0].patch).toEqual({ fontColor: '#e74c3c' })
163
+ })
164
+
165
+ test('textAlign / textBaseline', () => {
166
+ const r = buildSetFontOps({
167
+ refids: [1],
168
+ textAlign: 'center',
169
+ textBaseline: 'middle'
170
+ })
171
+ expect(r.ops[0].patch).toEqual({
172
+ textAlign: 'center',
173
+ textBaseline: 'middle'
174
+ })
175
+ })
176
+
177
+ test('lineHeight 양수', () => {
178
+ const r = buildSetFontOps({ refids: [1], lineHeight: 1.5 })
179
+ expect(r.ops[0].patch).toEqual({ lineHeight: 1.5 })
180
+ })
181
+
182
+ test('전체 필드 한 번에', () => {
183
+ const r = buildSetFontOps({
184
+ refids: [1],
185
+ fontFamily: 'Helvetica',
186
+ fontSize: 14,
187
+ fontWeight: 'bold',
188
+ fontStyle: 'normal',
189
+ fontColor: 'navy',
190
+ textAlign: 'left',
191
+ textBaseline: 'alphabetic',
192
+ lineHeight: 1.2
193
+ })
194
+ expect(r.ops[0].patch).toEqual({
195
+ fontFamily: 'Helvetica',
196
+ fontSize: 14,
197
+ fontWeight: 'bold',
198
+ fontStyle: 'normal',
199
+ fontColor: '#000080',
200
+ textAlign: 'left',
201
+ textBaseline: 'alphabetic',
202
+ lineHeight: 1.2
203
+ })
204
+ })
205
+ })
206
+
207
+ describe('buildSetFontOps — multi-target', () => {
208
+ test('동일 폰트 스타일이 여러 refid 에 분배', () => {
209
+ const r = buildSetFontOps({
210
+ refids: [1, 2, 3],
211
+ fontSize: 18,
212
+ fontColor: 'black'
213
+ })
214
+ expect(r.ops).toHaveLength(3)
215
+ expect(r.ops[0].patch).toEqual({
216
+ fontSize: 18,
217
+ fontColor: '#000000'
218
+ })
219
+ expect(r.ops[2]).toMatchObject({
220
+ refid: 3,
221
+ patch: { fontSize: 18, fontColor: '#000000' }
222
+ })
223
+ })
224
+ })
225
+
226
+ describe('text-tools — 시나리오', () => {
227
+ test('테마 적용 — fontColor 만 (테마 정책 준수)', () => {
228
+ // 테마 요청 시 fontColor 만 변경, fontSize/fontFamily 는 안 건드림
229
+ const r = buildSetFontOps({ refids: [1, 2], fontColor: '#444' })
230
+ expect(r.ops[0].patch).toEqual({ fontColor: '#444444' })
231
+ expect((r.ops[0] as any).patch.fontSize).toBeUndefined()
232
+ expect((r.ops[0] as any).patch.fontFamily).toBeUndefined()
233
+ })
234
+
235
+ test('명시 폰트 변경 — 사용자가 요청한 경우만', () => {
236
+ const r = buildSetFontOps({
237
+ refids: [42],
238
+ fontFamily: 'Pretendard',
239
+ fontSize: 24,
240
+ fontWeight: 'bold'
241
+ })
242
+ expect(r.ops[0].patch).toEqual({
243
+ fontFamily: 'Pretendard',
244
+ fontSize: 24,
245
+ fontWeight: 'bold'
246
+ })
247
+ })
248
+
249
+ test('레이블 텍스트 변경', () => {
250
+ const r = buildSetTextOps({ refids: [1], text: '온도: 23°C' })
251
+ expect(r.ops[0].patch).toEqual({ text: '온도: 23°C' })
252
+ })
253
+ })