@things-factory/board-ai 10.0.0-beta.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLAN-styling-tools.md +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,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
|
+
})
|