@things-factory/board-ai 10.1.24 → 10.1.26

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 (107) hide show
  1. package/PLAN-styling-tools.md +1 -1
  2. package/dist-client/server/service/assistant.d.ts +5 -4
  3. package/dist-client/server/service/assistant.js +35 -35
  4. package/dist-client/server/service/assistant.js.map +1 -1
  5. package/dist-client/server/service/styling/clear-tools.d.ts +2 -2
  6. package/dist-client/server/service/styling/clear-tools.js.map +1 -1
  7. package/dist-client/server/service/styling/copy-tools.d.ts +3 -3
  8. package/dist-client/server/service/styling/copy-tools.js +1 -1
  9. package/dist-client/server/service/styling/copy-tools.js.map +1 -1
  10. package/dist-client/server/service/styling/effect-tools.d.ts +2 -2
  11. package/dist-client/server/service/styling/effect-tools.js.map +1 -1
  12. package/dist-client/server/service/styling/fill-tools.d.ts +3 -3
  13. package/dist-client/server/service/styling/fill-tools.js +1 -1
  14. package/dist-client/server/service/styling/fill-tools.js.map +1 -1
  15. package/dist-client/server/service/styling/material-tools.d.ts +2 -2
  16. package/dist-client/server/service/styling/material-tools.js.map +1 -1
  17. package/dist-client/server/service/styling/registry.d.ts +6 -6
  18. package/dist-client/server/service/styling/registry.js +2 -2
  19. package/dist-client/server/service/styling/registry.js.map +1 -1
  20. package/dist-client/server/service/styling/stroke-tools.d.ts +2 -2
  21. package/dist-client/server/service/styling/stroke-tools.js.map +1 -1
  22. package/dist-client/server/service/styling/text-tools.d.ts +2 -2
  23. package/dist-client/server/service/styling/text-tools.js.map +1 -1
  24. package/dist-client/server/service/types.d.ts +6 -148
  25. package/dist-client/server/service/types.js.map +1 -1
  26. package/dist-client/server/service/validation/board-model-schema.d.ts +262 -262
  27. package/dist-client/server/service/validation/board-model-schema.js +4 -4
  28. package/dist-client/server/service/validation/board-model-schema.js.map +1 -1
  29. package/dist-client/server/service/validation/tool-validation.js +5 -5
  30. package/dist-client/server/service/validation/tool-validation.js.map +1 -1
  31. package/dist-client/tsconfig.tsbuildinfo +1 -1
  32. package/dist-server/index.d.ts +2 -2
  33. package/dist-server/index.js +2 -2
  34. package/dist-server/index.js.map +1 -1
  35. package/dist-server/service/assistant.d.ts +5 -4
  36. package/dist-server/service/assistant.js +37 -37
  37. package/dist-server/service/assistant.js.map +1 -1
  38. package/dist-server/service/board-ai-resolver.js +2 -2
  39. package/dist-server/service/board-ai-resolver.js.map +1 -1
  40. package/dist-server/service/index.d.ts +0 -1
  41. package/dist-server/service/index.js +0 -1
  42. package/dist-server/service/index.js.map +1 -1
  43. package/dist-server/service/patch-entry/board-patch-subscription.js +1 -1
  44. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -1
  45. package/dist-server/service/patch-entry/patch-entry.d.ts +1 -1
  46. package/dist-server/service/patch-entry/patch-entry.js +1 -1
  47. package/dist-server/service/patch-entry/patch-entry.js.map +1 -1
  48. package/dist-server/service/styling/clear-tools.d.ts +2 -2
  49. package/dist-server/service/styling/clear-tools.js.map +1 -1
  50. package/dist-server/service/styling/copy-tools.d.ts +3 -3
  51. package/dist-server/service/styling/copy-tools.js +1 -1
  52. package/dist-server/service/styling/copy-tools.js.map +1 -1
  53. package/dist-server/service/styling/effect-tools.d.ts +2 -2
  54. package/dist-server/service/styling/effect-tools.js.map +1 -1
  55. package/dist-server/service/styling/fill-tools.d.ts +3 -3
  56. package/dist-server/service/styling/fill-tools.js +1 -1
  57. package/dist-server/service/styling/fill-tools.js.map +1 -1
  58. package/dist-server/service/styling/material-tools.d.ts +2 -2
  59. package/dist-server/service/styling/material-tools.js.map +1 -1
  60. package/dist-server/service/styling/registry.d.ts +6 -6
  61. package/dist-server/service/styling/registry.js +2 -2
  62. package/dist-server/service/styling/registry.js.map +1 -1
  63. package/dist-server/service/styling/stroke-tools.d.ts +2 -2
  64. package/dist-server/service/styling/stroke-tools.js.map +1 -1
  65. package/dist-server/service/styling/text-tools.d.ts +2 -2
  66. package/dist-server/service/styling/text-tools.js.map +1 -1
  67. package/dist-server/service/types.d.ts +6 -148
  68. package/dist-server/service/types.js.map +1 -1
  69. package/dist-server/service/validation/board-model-schema.d.ts +262 -262
  70. package/dist-server/service/validation/board-model-schema.js +4 -4
  71. package/dist-server/service/validation/board-model-schema.js.map +1 -1
  72. package/dist-server/service/validation/tool-validation.js +5 -5
  73. package/dist-server/service/validation/tool-validation.js.map +1 -1
  74. package/dist-server/tsconfig.tsbuildinfo +1 -1
  75. package/logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json +5 -0
  76. package/logs/connections-2026-09-17-16.log +0 -0
  77. package/package.json +8 -7
  78. package/server/index.ts +2 -2
  79. package/server/service/assistant-integration.test.ts +44 -44
  80. package/server/service/assistant-llm-smoke.test.ts +5 -5
  81. package/server/service/assistant.test.ts +86 -86
  82. package/server/service/assistant.ts +41 -43
  83. package/server/service/board-ai-resolver.ts +2 -2
  84. package/server/service/index.ts +0 -1
  85. package/server/service/patch-entry/board-patch-subscription.ts +1 -1
  86. package/server/service/patch-entry/patch-entry.ts +2 -2
  87. package/server/service/styling/clear-tools.ts +2 -2
  88. package/server/service/styling/copy-tools.ts +3 -3
  89. package/server/service/styling/effect-tools.ts +2 -2
  90. package/server/service/styling/fill-tools.test.ts +1 -1
  91. package/server/service/styling/fill-tools.ts +4 -4
  92. package/server/service/styling/material-tools.ts +2 -2
  93. package/server/service/styling/registry.ts +6 -6
  94. package/server/service/styling/stroke-tools.test.ts +1 -1
  95. package/server/service/styling/stroke-tools.ts +3 -3
  96. package/server/service/styling/text-tools.ts +2 -2
  97. package/server/service/tool-exposure.test.ts +7 -7
  98. package/server/service/types.ts +9 -110
  99. package/server/service/validation/board-model-schema.test.ts +3 -3
  100. package/server/service/validation/board-model-schema.ts +4 -4
  101. package/server/service/validation/tool-validation.ts +5 -5
  102. package/dist-server/service/apply-patch.d.ts +0 -47
  103. package/dist-server/service/apply-patch.js +0 -369
  104. package/dist-server/service/apply-patch.js.map +0 -1
  105. package/server/service/apply-patch-drift.test.ts +0 -831
  106. package/server/service/apply-patch.test.ts +0 -757
  107. package/server/service/apply-patch.ts +0 -411
@@ -200,7 +200,7 @@ tslib_1.__decorate([
200
200
  tslib_1.__metadata("design:type", String)
201
201
  ], BoardAIChatOutput.prototype, "reply", void 0);
202
202
  tslib_1.__decorate([
203
- (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, { nullable: true, description: 'BoardEditPatch.' }),
203
+ (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, { nullable: true, description: 'SceneEditPatch.' }),
204
204
  tslib_1.__metadata("design:type", Object)
205
205
  ], BoardAIChatOutput.prototype, "patch", void 0);
206
206
  tslib_1.__decorate([
@@ -237,7 +237,7 @@ tslib_1.__decorate([
237
237
  tslib_1.__decorate([
238
238
  (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
239
239
  nullable: true,
240
- description: 'Ephemeral scene actions (selection/view/mode) — sequence of BoardActionOp. Applied via board-action-execute event on the host. Distinct from `patch` which carries persistent model changes.'
240
+ description: 'Ephemeral scene actions (selection/view/mode) — sequence of SceneActionOp. Applied via scene-action-execute event on the host. Distinct from `patch` which carries persistent model changes.'
241
241
  }),
242
242
  tslib_1.__metadata("design:type", Object)
243
243
  ], BoardAIChatOutput.prototype, "actions", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"board-ai-resolver.js","sourceRoot":"","sources":["../../server/service/board-ai-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GraphQL resolver — AI 주도 보드 모델링 (chat).\n *\n * Mutation: boardAIChat — 자연어 채팅. sessionId 가 있으면 메시지/패치 영속.\n * sessionId 가 없으면 ad-hoc 모드로 단발 호출 (영속 없음).\n */\nimport { Resolver, Mutation, Arg, Ctx, Directive, Field, InputType, Int, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport { MoreThan } from 'typeorm'\nimport '@things-factory/auth-base'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { createAIClient, getDefaultAIClient, type AIClient } from '@things-factory/ai-client-base'\nimport type { ComponentCategory } from '@things-factory/board-import'\n\nimport { DefaultBoardAIAssistant } from './assistant.js'\nimport { effectiveBoardTools } from './board-tool-rights.js'\nimport type { LLMMessage } from './types.js'\nimport { ChatSession } from '@things-factory/ai-assistant'\nimport { ChatMessage } from '@things-factory/ai-assistant'\nimport { buildLlmHistory } from '@things-factory/ai-assistant'\nimport { splitByCap, summaryInstruction } from '@things-factory/ai-assistant'\nimport { foldHistorySummary } from '@things-factory/ai-assistant'\n\n/**\n * 모델에 넘길 최근 메시지 수 상한.\n *\n * 협의는 여럿이 며칠에 걸쳐 이어진다 — 상한 없이 전체를 매 턴 보내면 프롬프트가 무한히 자라고\n * (비용·지연·문맥 한계) 오래된 말이 최근 상황을 덮는다. 40 은 첫 경계선이다: 한 자리의 협의를\n * 담기에 넉넉하고, 넘치면 생략 사실을 모델에 알린다(조용히 버리지 않는다).\n * 버린 앞부분을 **요약해서** 실어 보내는 것이 다음 단계다(ChatSession.lastSummary 미사용 상태).\n */\nconst LLM_HISTORY_MAX_TURNS = 40\n\n/**\n * 요약을 다시 만드는 임계치 — 상한 밖으로 **새로 밀려난** 메시지 수.\n *\n * 상한을 넘긴 뒤 매 턴 요약을 만들면 대화마다 LLM 호출이 두 번씩 붙는다. 10 개씩 모아 접는다.\n */\nconst LLM_SUMMARY_THRESHOLD = 10\nimport { activityPatch, autoSessionName } from '@things-factory/ai-assistant'\nimport { publishSessionActivity } from '@things-factory/ai-assistant'\nimport { publishChatMessage } from '@things-factory/ai-assistant'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\nimport { ChatSessionParticipant } from '@things-factory/ai-assistant'\nimport { buildInviteNotification } from '@things-factory/ai-assistant'\nimport {\n injectMentionRefids,\n injectAiMentionRefids,\n stripMentionRefids,\n extractMentionUserIds,\n newParticipantUserIds\n} from './mention-marker.js'\n\n@InputType()\nclass BoardAILLMMessageInput {\n @Field({ description: \"Role: 'user' or 'assistant'\" })\n role: string\n\n @Field({ description: 'Message content' })\n content: string\n}\n\n@InputType()\nclass BoardAIMentionPickInput {\n @Field({ description: 'Token used by user (e.g. \"경광등1\" — the part after #).' })\n token: string\n\n @Field(() => Int, {\n description:\n 'Refid explicitly picked from popup. Server uses this for deterministic resolution — bypasses fallback name/id/type matching for this token.'\n })\n refid: number\n}\n\n@InputType()\nclass BoardAIChatInput {\n @Field({\n nullable: true,\n description: 'ChatSession id. Omit for ad-hoc (no persistence). When given, messages/patches are persisted.'\n })\n sessionId?: string\n\n @Field(() => [BoardAILLMMessageInput], {\n description: 'Conversation history (latest user message at the end). For persisted sessions, only the last user message is appended; older are loaded from DB.'\n })\n messages: BoardAILLMMessageInput[]\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Current BoardModel JSON.' })\n currentBoard?: any\n\n @Field(() => [String], {\n nullable: true,\n description:\n \"Capability scopes the AI is allowed to use (e.g. 'create', 'edit', 'style'). Filters which tools are exposed to the LLM.\"\n })\n scopes?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Host context describing what this surface is looking at (e.g. { spaceId } for a space deliberation, { boardId } for board authoring). Tools read it as ctx.state.host so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; tools verify the scope belongs to the caller tenant.'\n })\n hostContext?: any\n\n @Field({\n nullable: true,\n description:\n 'Id of the last message to keep from the stored history. The client keeps its own truncation when a user edits a message (the server history is intentionally preserved), so pass the last kept message id to hide the retracted span from the agent. Omit to use the whole stored history.'\n })\n truncateAfterMessageId?: string\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Registered external tool categories this conversation may use (registerToolCategory names, e.g. [\"twin\", \"board-import\"]). Omit to allow every registered category. Applies to tool definitions AND execution, so a surface cannot call a category it was not granted.'\n })\n toolCategories?: string[]\n\n @Field({\n nullable: true,\n description:\n 'Whether core board editing tools (addComponent / styling / scene actions) are available. Defaults to true. Set false for surfaces that must not modify the board — e.g. an operations chat where irreversible commands live, so reversible edits and irreversible actions never share one conversation.'\n })\n boardTools?: boolean\n\n @Field({\n nullable: true,\n description:\n 'Force a tool call on the first turn (toolChoice: required). Turn this on for surfaces that discuss live state: the grounding guard catches invented identifiers but not an invented situation stated without any, which only happens when the model answers without querying at all. Concept / how-to questions are unaffected — they are answered through a documentation tool.'\n })\n requireGroundingTools?: boolean\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component types the LLM is allowed to emit (e.g. [\"rect\", \"label\"]). Constrains generation to the current solution.'\n })\n knownTypes?: string[]\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component categories to surface (e.g. [\"3D\", \"form\"]). Subset filter for groupings exposed in component-schemas.'\n })\n categories?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Component schemas — array of { type, description?, group?, properties? } so the LLM knows valid props per type.'\n })\n componentSchemas?: any\n\n @Field(() => [Int], {\n nullable: true,\n description:\n 'Refids of components currently selected in the modeller (universal numeric handle, things-scene auto-assigned). For \"selected\" / \"선택한\" intent. Note: distinct from `id` which is a data-binding name.'\n })\n selectedRefids?: number[]\n\n @Field(() => [BoardAIMentionPickInput], {\n nullable: true,\n description:\n 'Explicit #mention picks from popup — {token, refid}. User text stays clean (#token only); these picks let server resolve deterministically without exposing refid in the user-visible message. Tokens not in this list fall back to free name/id/type matching.'\n })\n mentions?: BoardAIMentionPickInput[]\n\n @Field({\n nullable: true,\n description: 'AI model identifier override (e.g. \"claude-opus-4-7\"). Falls back to the configured default when omitted.'\n })\n model?: string\n\n @Field({\n nullable: true,\n description: 'Sampling temperature override. Falls back to assistant default when omitted.'\n })\n temperature?: number\n\n @Field({\n nullable: true,\n description: 'Max output tokens override per LLM call. Falls back to assistant default when omitted.'\n })\n maxTokens?: number\n}\n\n@ObjectType()\nclass BoardAIChatOutput {\n @Field({ description: 'Conversational reply.' })\n reply: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'BoardEditPatch.' })\n patch?: any\n\n @Field({ nullable: true, description: 'Clarifying question when input is ambiguous.' })\n followUp?: string\n\n @Field({ description: 'AI client identifier (provider:model).' })\n clientId: string\n\n @Field({ nullable: true, description: 'Echo of session id (when persisted).' })\n sessionId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the user input.' })\n userMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the AI reply.' })\n assistantMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted PatchEntry id (when patch was generated).' })\n patchId?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usages collected during agentic loop — sequence of {name, arguments, result, kind}. UI fold-able box for transparency / debug.'\n })\n toolUsages?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Ephemeral scene actions (selection/view/mode) — sequence of BoardActionOp. Applied via board-action-execute event on the host. Distinct from `patch` which carries persistent model changes.'\n })\n actions?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Reported when the stored history exceeded the prompt cap for this turn: { omitted, summarized }. The surface uses it to tell the user the conversation grew long — a new conversation is better for a different topic. Objective fact only; no topic-change detection.'\n })\n historyFolded?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Action proposals the AI recorded without executing — each carries what a tool returned alongside `proposed: true`. The surface renders a confirm button; execution is the user\\'s act, never the assistant\\'s. Null when nothing was proposed.'\n })\n proposals?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Ungrounded identifiers mentioned by the reply — present in the answer but absent from everything the model received (prompt, board context, history, tool results). Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'\n })\n groundingWarnings?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Grounded analysis contract for this turn: scope, evidence-backed claims, limitations, proposal and explicit approval boundary. Null when the turn did not perform an analysis.'\n })\n analysis?: any\n}\n\n@Resolver()\nexport class BoardAIChatResolver {\n /**\n * NOTE: 일부러 @Directive('@transaction') 사용하지 않는다.\n * LLM 호출은 수 초~수십 초 — 트랜잭션 안에서 호출하면 SQLite write lock 을\n * 그 시간 동안 보유 → 다른 작업 (cache 정리 등) 과 SQLITE_BUSY 충돌.\n * 따라서 DB 쓰기를 LLM 호출 전후의 짧은 단위로 나누고 각각 auto-commit.\n * 데이터 일관성: 채팅 이력은 부분 누락보다 부분 보존이 안전 (대화 컨텍스트).\n */\n @Mutation(() => BoardAIChatOutput, {\n description: 'AI 주도 보드 모델링 — 자연어 채팅으로 보드 생성·구조편집·스타일링. sessionId 로 영속 컨텍스트.'\n })\n /*\n * ── 이 문은 「묻는 것」이지 「보드를 고치는 것」이 아니다 (2026-09-10) ──────────\n * 공장 현황을 묻는 화면이 **보드 편집 권한**을 요구하던 것이 이 분리의 시작이었다. 대화는\n * `ai-assistant:mutation`, 보드 편집은 `board-ai:mutation` — 둘은 다른 능력이다.\n *\n * ⚠ 문만 바꾸면 **거꾸로 넓어진다.** 코어 보드 편집 도구가 기본으로 켜져 있으므로, 대화 권한만\n * 가진 사람이 이 문으로 보드를 고치게 된다. 그래서 아래에서 **보드 편집 권한을 따로 보고**\n * 없으면 그 도구를 끈다(§`effectiveBoardTools`).\n */\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\")')\n async boardAIChat(\n @Arg('input') input: BoardAIChatInput,\n @Ctx() context: ResolverContext\n ): Promise<BoardAIChatOutput> {\n const base = resolveAIClient()\n if (!base) {\n throw new Error(\n 'No AI client configured. Set config.aiClient = { provider: \"anthropic\" | \"gemini\", model, apiKey } or call setDefaultAIClient(...).'\n )\n }\n\n const { domain, user } = context.state\n const sessionRepo = getRepository(ChatSession)\n const messageRepo = getRepository(ChatMessage)\n const patchRepo = getRepository(PatchEntry)\n\n // ── Phase A: LLM 호출 전 짧은 DB 쓰기 ─────────────────────────\n let session: ChatSession | undefined\n if (input.sessionId) {\n const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } })\n if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)\n session = found\n }\n\n const lastUserMsg = input.messages[input.messages.length - 1]\n let userMessageId: string | undefined\n if (session && lastUserMsg && lastUserMsg.role === 'user') {\n // popup pick 의 refid 를 #token{refid:N} inline marker 로 content 에 박아 저장.\n // 단일 source-of-truth — 별도 컬럼 없이 content 안에서 token-refid 매핑 보존.\n const enriched = injectMentionRefids(lastUserMsg.content, input.mentions)\n // parentMessage = 이 세션의 직전 메시지 (보통 assistant / system / 또는 첫 turn 이면 null).\n // tree 구조 인프라 — branching (regenerate / 사용자별 분기) 는 같은 부모 child 로.\n const previousLast = await messageRepo.findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const saved = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'user',\n content: enriched,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n })\n userMessageId = saved.id\n\n /* 세션 활동 갱신(비정규화) — 목록 최근활동순 정렬과 다른 참여자의 안 읽음 판정 기준.\n * 이름이 아직 없으면 **첫 발화로 자동 작명** 한다 — `세션 2` 보다 \"A-DC 포화 원인\" 이 목록에서\n * 훨씬 유용하다. 사용자가 직접 바꾼 이름은 여기서 건드리지 않는다(이름이 있으면 그대로).\n * best-effort: 실패해도 대화를 막지 않는다(목록 정렬이 한 칸 늦을 뿐). */\n try {\n const patch: any = activityPatch(enriched)\n if (!session.name) patch.name = autoSessionName(enriched)\n await sessionRepo.update(session.id!, patch)\n } catch {\n /* noop */\n }\n /* 내가 참여한 세션 활동 방송 — 창을 닫아 둔 다른 참여자의 배지·목록을 위해. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enriched,\n actorId: user?.id\n })\n\n // C4: 사용자 메시지를 같은 세션 구독자에게 방송 (그룹챗 실시간). best-effort.\n // 자기 발신 에코는 클라이언트가 message.id 로 dedup.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved })\n\n // C4: @user 멘션 → 세션 참여자(member) 자동 등록 (초대). best-effort — 실패해도 chat 흐름 무영향.\n // enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자/자기 제외한 신규만 member 로 등록.\n try {\n await this._inviteMentionedUsers(enriched, session.id!, domain.id, user, session.boardId, (context.state as any).notify)\n } catch {\n /* 초대 실패는 대화를 막지 않는다 */\n }\n }\n\n // ── Phase B: LLM 호출 (DB 잠금 없음, 오래 걸려도 OK) ───────────\n const assistant = new DefaultBoardAIAssistant(base, {\n scopes: input.scopes,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined\n })\n\n /* 에이전트가 볼 이력 — **세션이 있으면 서버 정본(ChatMessage)** 을 쓴다.\n * 클라이언트가 보낸 줄을 쓰면 협의에서 참여자마다 화면이 달라 에이전트가 사람마다 다른 대화를 본다.\n * 발신자 표기·system 기록 포함·접은 구간 제외 규칙은 buildLlmHistory 가 소유(그 파일 머리말 참조).\n * 즉석 대화(sessionId 없음)는 영속 이력이 없으므로 클라이언트 이력을 그대로 쓴다.\n *\n * 정렬은 createdAt ASC + id ASC — 같은 시각 삽입의 순서까지 결정적으로 만든다(조립 쪽에서\n * 다시 정렬하지 않으므로 여기서 확정해야 한다). 멘션 마커는 여기서 제거해 넘긴다. */\n let llmMessages: LLMMessage[]\n let historyFolded: { omitted: number; summarized: boolean } | undefined\n if (session) {\n const rows = await messageRepo.find({\n where: { session: { id: session.id } as any },\n relations: { creator: true },\n order: { createdAt: 'ASC', id: 'ASC' }\n })\n /* 이번 턴에 상한 밖으로 밀려난 것이 있는가 — 화면이 \"대화가 길어졌다\" 를 알릴 근거(사실만). */\n const capped = splitByCap(\n rows.map(row => ({ id: row.id, role: row.role, content: row.content ?? '' })),\n LLM_HISTORY_MAX_TURNS\n )\n if (capped.dropped.length > 0) {\n historyFolded = { omitted: capped.dropped.length, summarized: !!(session as any).lastSummary }\n }\n llmMessages = buildLlmHistory(\n rows.map(row => ({\n id: row.id,\n role: row.role,\n content: stripMentionRefids(row.content ?? ''),\n senderId: (row as any).creator?.id,\n /* 표시 이름만 — 이메일 등 개인정보는 프롬프트에 넣지 않는다. */\n senderName: (row as any).creator?.name || (row as any).creator?.username\n })),\n {\n truncateAfterMessageId: input.truncateAfterMessageId ?? undefined,\n /* 방금 저장한 사용자 메시지는 접은 구간보다 뒤에 있어도 반드시 남긴다. */\n keepMessageIds: userMessageId ? [userMessageId] : [],\n /* 이력 상한 — 협의는 며칠 이어진다. 상한이 없으면 매 턴 전체를 보내 프롬프트가 무한히 자란다. */\n maxTurns: LLM_HISTORY_MAX_TURNS,\n /* 밀려난 앞부분은 요약으로 대체한다(있으면). 요약은 응답 뒤에 갱신한다 — 아래 Phase D. */\n summary: { text: (session as any).lastSummary, upToMessageId: (session as any).summaryUpToMessageId }\n }\n )\n } else {\n llmMessages = input.messages.map(m => ({\n role: m.role === 'assistant' ? 'assistant' : 'user',\n content: stripMentionRefids(m.content)\n }))\n }\n\n const r = await assistant.chat(llmMessages, input.currentBoard ?? undefined, {\n scopes: input.scopes,\n hostContext: input.hostContext ?? undefined,\n /* 대화면별 도구 노출 — 미지정이면 현행(전부 허용) 그대로. */\n toolCategories: input.toolCategories ?? undefined,\n boardTools: await effectiveBoardTools(input.boardTools, context),\n requireGroundingTools: input.requireGroundingTools ?? undefined,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined,\n selectedRefids: input.selectedRefids,\n mentionPicks: input.mentions,\n componentSchemas: Array.isArray(input.componentSchemas) ? input.componentSchemas : undefined,\n model: input.model,\n temperature: input.temperature ?? undefined,\n maxTokens: input.maxTokens ?? undefined,\n // 외부 도메인 (board-import 등) 의 read/external tool builder 가 server-side\n // 의존성 (도메인 / 유저 / 트랜잭션) 을 사용할 수 있도록 ResolverContext.state 전달.\n // board-ai 코어 자체는 이 값을 알지 않고 dispatch 단계에서 closure 로만 흘림.\n toolCallContext: context.state\n })\n\n // ── Phase C: LLM 호출 후 짧은 DB 쓰기 ─────────────────────────\n let assistantMessageId: string | undefined\n let patchId: string | undefined\n if (session) {\n let patchEntry: PatchEntry | undefined\n if (r.patch) {\n patchEntry = await patchRepo.save({\n session: { id: session.id } as any,\n source: 'ai',\n ops: JSON.stringify(r.patch.ops),\n summary: r.patch.summary,\n confidence: r.patch.confidence,\n reverted: false\n })\n patchId = patchEntry.id\n // C1: 같은 board 구독자에게 방송 (실시간 공동편집 spine). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: patchEntry })\n }\n\n // AI 응답의 #token 들을 currentBoard 에 대해 resolve 한 뒤 inline marker 로 박음.\n // patch 로 새로 만든 컴포넌트는 input.currentBoard 에 없으므로 marker 없이 저장 →\n // 클릭 시 runtime fallback 으로 해석 (그때는 patch 적용 후라 scene 에 존재).\n const enrichedReply = injectAiMentionRefids(r.reply, input.currentBoard ?? null)\n\n const aiMsg = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'assistant',\n content: enrichedReply,\n relatedPatchId: patchEntry?.id,\n toolUsages: r.toolUsages && r.toolUsages.length > 0 ? JSON.stringify(r.toolUsages) : undefined,\n /* 접지 경고를 함께 영속 — 나중에 읽는 참여자도 같은 주의를 봐야 한다(재계산 불가: 근거 원문이 남지 않는다).\n * 컬럼 길이(1024) 안에 들도록 앞 20개까지만 — 그 이상이면 목록이 아니라 답 자체가 문제다. */\n groundingWarnings:\n r.groundingWarnings && r.groundingWarnings.length > 0\n ? JSON.stringify(r.groundingWarnings.slice(0, 20))\n : undefined,\n // assistant 메시지의 creator = 이 응답을 트리거한 user (= 직전 user 메시지의 작성자).\n // 같은 turn 으로 묶임 → 향후 사용자별 필터에서 user/assistant 가 함께 따라옴.\n creator: user,\n updater: user,\n // parentMessage = 이 응답이 답하는 user 메시지. regenerate 시 같은 user 메시지를\n // 부모로 하는 형제 assistant 들이 생기면 자연스럽게 branching tree.\n parentMessage: userMessageId ? ({ id: userMessageId } as any) : undefined\n })\n assistantMessageId = aiMsg.id\n\n // C4: AI 응답도 같은 세션 구독자에게 방송 (트리거한 본인 포함 — id dedup). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: aiMsg })\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\" 으로 보인다. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enrichedReply,\n actorId: user?.id\n })\n\n await sessionRepo.update(session.id, {\n aiClientId: base.id,\n updater: user,\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\"으로 보인다. */\n ...activityPatch(enrichedReply)\n })\n\n /* ── Phase D: 이력 요약 접기 ────────────────────────────────────────────\n * 응답을 **돌려준 뒤** 갱신한다 — 사용자 대기 시간에 요약용 LLM 호출을 얹지 않는다.\n * 기다리지 않으므로(await 없음) 실패해도 대화에는 영향이 없다: 요약이 없으면 다음 턴은\n * \"N개 생략\" 으로 정직하게 알리고 넘어간다(조용한 맥락 상실이 아니다). */\n void this._foldHistorySummary(session.id!, domain.id, base)\n }\n\n return {\n reply: r.reply,\n patch: r.patch ?? null,\n followUp: r.followUp ?? null,\n clientId: base.id,\n sessionId: session?.id,\n userMessageId,\n assistantMessageId,\n patchId,\n toolUsages: r.toolUsages ?? null,\n actions: r.actions && r.actions.length > 0 ? r.actions : null,\n proposals: r.proposals && r.proposals.length > 0 ? r.proposals : null,\n historyFolded: historyFolded ?? null,\n groundingWarnings: r.groundingWarnings && r.groundingWarnings.length > 0 ? r.groundingWarnings : null,\n analysis: r.analysis ?? null\n }\n }\n\n /**\n * 이력 요약 접기 — 상한 밖으로 밀려난 앞부분을 요약으로 눌러 담는다.\n *\n * 왜 이렇게 나눠 두었나: 판정(무엇을 요약할지·다시 만들 때인지)은 순수 로직(history-summary)이\n * 갖고, 여기서는 **호출과 저장**만 한다. 그래야 규칙을 테스트로 고정할 수 있다.\n *\n * 아껴 만든다: 새로 밀려난 것이 임계치를 넘을 때만 LLM 을 부른다(상한을 넘긴 뒤 매 턴 부르면\n * 대화마다 호출이 두 번씩 붙는다). 실패는 조용히 넘긴다 — 다음 턴이 \"생략\" 으로 알리고,\n * 다음 임계치에서 다시 시도한다.\n */\n private async _foldHistorySummary(\n sessionId: string,\n domainId: string,\n client: AIClient\n ): Promise<void> {\n try {\n const sessionRepo = getRepository(ChatSession)\n const session = await sessionRepo.findOneBy({ id: sessionId, domain: { id: domainId } as any })\n if (!session) return\n\n const stored = {\n text: (session as any).lastSummary,\n upToMessageId: (session as any).summaryUpToMessageId\n }\n const { rows, startsAfterSummary } = await this._historyForFold(sessionId, stored)\n const history = rows.map(row => ({\n id: row.id,\n role: row.role,\n content: stripMentionRefids(row.content ?? ''),\n senderId: (row as any).creator?.id,\n senderName: (row as any).creator?.name || (row as any).creator?.username\n }))\n const { patch } = await foldHistorySummary({\n rows: history,\n rowsStartAfterSummary: startsAfterSummary,\n stored,\n maxTurns: LLM_HISTORY_MAX_TURNS,\n threshold: LLM_SUMMARY_THRESHOLD,\n summarize: content =>\n client.chat([{ role: 'user', content }], {\n systemPrompt: summaryInstruction(),\n maxTokens: 700,\n /* 요약은 창작이 아니다 — 흔들림을 줄인다. */\n temperature: 0\n })\n })\n if (patch) await sessionRepo.update(sessionId, patch as any)\n } catch {\n /* 요약 실패는 대화를 막지 않는다 — 다음 턴은 \"생략\" 으로 정직하게 알린다. */\n }\n }\n\n /**\n * 요약에 필요한 만큼만 읽는다 — **요약이 덮은 지점 뒤부터.**\n *\n * ── 왜 (2026-08-20) ─────────────────────────────────────────────────────────\n * 이 함수를 부르는 자리는 **메시지를 보낼 때마다**다. 예전에는 세션의 대화를 처음부터 끝까지 읽었고\n * (`creator` 조인까지), 정작 쓰는 것은 두 조각뿐이었다: 마지막 `maxTurns`개와, 요약 워터마크 뒤부터의\n * 미반영분. 대화가 2,000개면 매 메시지마다 2,000행을 읽어 수십 행을 쓴 셈이다.\n *\n * 워터마크 뒤부터 읽으면 그 구간이 곧 `미반영분 + 최근 유지분`이라, 읽는 행 수가 **대화 길이와\n * 무관**해진다(정상 상태에서 대략 `threshold + maxTurns`).\n *\n * ── 안전 규율을 지킨다 ──────────────────────────────────────────────────────\n * 워터마크 행이 지워졌거나(삭제·편집) 요약이 아직 없으면 **전체를 읽는다.** 그때는 요약이 어디까지\n * 덮는지 알 수 없고, 「빠뜨리는 쪽보다 다시 요약하는 쪽이 안전하다」는 것이 이 기능의 규율이다\n * (`pendingForSummary` 주석). 그 경우는 드물다.\n *\n * 순서는 `(createdAt, id)` — 읽는 순서와 자르는 기준이 같아야 한다. 같은 시각의 메시지가 있으면\n * id 로 가른다(원래 정렬이 이미 그 규약이다).\n */\n private async _historyForFold(\n sessionId: string,\n stored: { text?: string | null; upToMessageId?: string | null }\n ): Promise<{ rows: ChatMessage[]; startsAfterSummary: boolean }> {\n const repo = getRepository(ChatMessage)\n const session = { id: sessionId } as any\n const order = { createdAt: 'ASC', id: 'ASC' } as any\n const relations = { creator: true } as any\n\n if (stored.text && stored.upToMessageId) {\n const mark = await repo.findOne({ where: { session, id: stored.upToMessageId } })\n if (mark?.createdAt) {\n const rows = await repo.find({\n /* 배열 = OR. 같은 시각 안에서는 id 로 이어 자른다(정렬 규약과 같은 축). */\n where: [\n { session, createdAt: MoreThan(mark.createdAt) },\n { session, createdAt: mark.createdAt, id: MoreThan(mark.id) }\n ],\n relations,\n order\n })\n return { rows, startsAfterSummary: true }\n }\n }\n\n /* 워터마크가 없거나 그 행이 사라졌다 — 덮은 범위를 모르므로 전부 다시 본다. */\n return { rows: await repo.find({ where: { session }, relations, order }), startsAfterSummary: false }\n }\n\n /**\n * C4: 메시지에 @멘션된 사용자를 세션 참여자(member)로 자동 등록 — 초대.\n * enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자·자기 제외한 신규만 등록.\n * 소프트삭제(deletedAt)된 과거 참여자는 다시 살린다(재초대). 신규 등록 후 협업 초대 알림 발송(C4b-2, best-effort).\n */\n private async _inviteMentionedUsers(\n content: string,\n sessionId: string,\n domainId: string,\n inviter: any,\n boardId?: string,\n notify?: (arg: any) => Promise<void>\n ): Promise<void> {\n const mentioned = extractMentionUserIds(content)\n if (mentioned.length === 0) return\n\n const repo = getRepository(ChatSessionParticipant)\n const existing = await repo.find({\n where: { session: { id: sessionId } as any, domain: { id: domainId } as any },\n withDeleted: true\n })\n const activeIds = existing.filter(p => !p.deletedAt).map(p => p.userId!).filter(Boolean)\n const fresh = newParticipantUserIds(mentioned, activeIds, inviter?.id)\n if (fresh.length === 0) return\n\n for (const userId of fresh) {\n // 과거에 나갔던(soft-deleted) 참여자면 되살리고, 아니면 새로 등록.\n const prior = existing.find(p => p.userId === userId && p.deletedAt)\n if (prior) {\n await repo.update(prior.id!, { deletedAt: null as any, role: 'member', updater: inviter } as any)\n } else {\n await repo.save({\n domain: { id: domainId } as any,\n session: { id: sessionId } as any,\n user: { id: userId } as any,\n role: 'member',\n creator: inviter,\n updater: inviter\n } as any)\n }\n }\n\n // C4b-2: 초대 알림 — notification 모듈의 기존 발송 메커니즘 재사용(중복 구현 금지).\n // context.state.notify 는 notification-middleware 가 전역 주입(FCM/pubsub). duck-typed →\n // board-ai 에 의존성 추가 없이 런타임 호출. 미로드 환경이면 undefined → graceful skip.\n this._notifyInvited(notify, fresh, boardId, inviter)\n }\n\n /** 초대된 사용자들에게 협업 초대 알림 발송 (best-effort, capability 있을 때만). */\n private _notifyInvited(\n notify: ((arg: any) => Promise<void>) | undefined,\n userIds: string[],\n boardId: string | undefined,\n inviter: any\n ): void {\n if (typeof notify !== 'function') return\n const payload = buildInviteNotification(userIds, boardId, inviter)\n if (!payload) return\n Promise.resolve(notify(payload)).catch(() => {\n /* 알림 실패는 초대·대화를 막지 않는다 */\n })\n }\n}\n\nfunction resolveAIClient(): AIClient | undefined {\n const existing = getDefaultAIClient()\n if (existing) return existing\n const cfg = config.get('aiClient', null) as any\n if (cfg && cfg.provider) {\n return createAIClient(cfg)\n }\n return undefined\n}\n"]}
1
+ {"version":3,"file":"board-ai-resolver.js","sourceRoot":"","sources":["../../server/service/board-ai-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GraphQL resolver — AI 주도 보드 모델링 (chat).\n *\n * Mutation: boardAIChat — 자연어 채팅. sessionId 가 있으면 메시지/패치 영속.\n * sessionId 가 없으면 ad-hoc 모드로 단발 호출 (영속 없음).\n */\nimport { Resolver, Mutation, Arg, Ctx, Directive, Field, InputType, Int, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport { MoreThan } from 'typeorm'\nimport '@things-factory/auth-base'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { createAIClient, getDefaultAIClient, type AIClient } from '@things-factory/ai-client-base'\nimport type { ComponentCategory } from '@things-factory/board-import'\n\nimport { DefaultBoardAIAssistant } from './assistant.js'\nimport { effectiveBoardTools } from './board-tool-rights.js'\nimport type { LLMMessage } from './types.js'\nimport { ChatSession } from '@things-factory/ai-assistant'\nimport { ChatMessage } from '@things-factory/ai-assistant'\nimport { buildLlmHistory } from '@things-factory/ai-assistant'\nimport { splitByCap, summaryInstruction } from '@things-factory/ai-assistant'\nimport { foldHistorySummary } from '@things-factory/ai-assistant'\n\n/**\n * 모델에 넘길 최근 메시지 수 상한.\n *\n * 협의는 여럿이 며칠에 걸쳐 이어진다 — 상한 없이 전체를 매 턴 보내면 프롬프트가 무한히 자라고\n * (비용·지연·문맥 한계) 오래된 말이 최근 상황을 덮는다. 40 은 첫 경계선이다: 한 자리의 협의를\n * 담기에 넉넉하고, 넘치면 생략 사실을 모델에 알린다(조용히 버리지 않는다).\n * 버린 앞부분을 **요약해서** 실어 보내는 것이 다음 단계다(ChatSession.lastSummary 미사용 상태).\n */\nconst LLM_HISTORY_MAX_TURNS = 40\n\n/**\n * 요약을 다시 만드는 임계치 — 상한 밖으로 **새로 밀려난** 메시지 수.\n *\n * 상한을 넘긴 뒤 매 턴 요약을 만들면 대화마다 LLM 호출이 두 번씩 붙는다. 10 개씩 모아 접는다.\n */\nconst LLM_SUMMARY_THRESHOLD = 10\nimport { activityPatch, autoSessionName } from '@things-factory/ai-assistant'\nimport { publishSessionActivity } from '@things-factory/ai-assistant'\nimport { publishChatMessage } from '@things-factory/ai-assistant'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\nimport { ChatSessionParticipant } from '@things-factory/ai-assistant'\nimport { buildInviteNotification } from '@things-factory/ai-assistant'\nimport {\n injectMentionRefids,\n injectAiMentionRefids,\n stripMentionRefids,\n extractMentionUserIds,\n newParticipantUserIds\n} from './mention-marker.js'\n\n@InputType()\nclass BoardAILLMMessageInput {\n @Field({ description: \"Role: 'user' or 'assistant'\" })\n role: string\n\n @Field({ description: 'Message content' })\n content: string\n}\n\n@InputType()\nclass BoardAIMentionPickInput {\n @Field({ description: 'Token used by user (e.g. \"경광등1\" — the part after #).' })\n token: string\n\n @Field(() => Int, {\n description:\n 'Refid explicitly picked from popup. Server uses this for deterministic resolution — bypasses fallback name/id/type matching for this token.'\n })\n refid: number\n}\n\n@InputType()\nclass BoardAIChatInput {\n @Field({\n nullable: true,\n description: 'ChatSession id. Omit for ad-hoc (no persistence). When given, messages/patches are persisted.'\n })\n sessionId?: string\n\n @Field(() => [BoardAILLMMessageInput], {\n description: 'Conversation history (latest user message at the end). For persisted sessions, only the last user message is appended; older are loaded from DB.'\n })\n messages: BoardAILLMMessageInput[]\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Current BoardModel JSON.' })\n currentBoard?: any\n\n @Field(() => [String], {\n nullable: true,\n description:\n \"Capability scopes the AI is allowed to use (e.g. 'create', 'edit', 'style'). Filters which tools are exposed to the LLM.\"\n })\n scopes?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Host context describing what this surface is looking at (e.g. { spaceId } for a space deliberation, { boardId } for board authoring). Tools read it as ctx.state.host so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; tools verify the scope belongs to the caller tenant.'\n })\n hostContext?: any\n\n @Field({\n nullable: true,\n description:\n 'Id of the last message to keep from the stored history. The client keeps its own truncation when a user edits a message (the server history is intentionally preserved), so pass the last kept message id to hide the retracted span from the agent. Omit to use the whole stored history.'\n })\n truncateAfterMessageId?: string\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Registered external tool categories this conversation may use (registerToolCategory names, e.g. [\"twin\", \"board-import\"]). Omit to allow every registered category. Applies to tool definitions AND execution, so a surface cannot call a category it was not granted.'\n })\n toolCategories?: string[]\n\n @Field({\n nullable: true,\n description:\n 'Whether core board editing tools (addComponent / styling / scene actions) are available. Defaults to true. Set false for surfaces that must not modify the board — e.g. an operations chat where irreversible commands live, so reversible edits and irreversible actions never share one conversation.'\n })\n boardTools?: boolean\n\n @Field({\n nullable: true,\n description:\n 'Force a tool call on the first turn (toolChoice: required). Turn this on for surfaces that discuss live state: the grounding guard catches invented identifiers but not an invented situation stated without any, which only happens when the model answers without querying at all. Concept / how-to questions are unaffected — they are answered through a documentation tool.'\n })\n requireGroundingTools?: boolean\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component types the LLM is allowed to emit (e.g. [\"rect\", \"label\"]). Constrains generation to the current solution.'\n })\n knownTypes?: string[]\n\n @Field(() => [String], {\n nullable: true,\n description: 'Component categories to surface (e.g. [\"3D\", \"form\"]). Subset filter for groupings exposed in component-schemas.'\n })\n categories?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Component schemas — array of { type, description?, group?, properties? } so the LLM knows valid props per type.'\n })\n componentSchemas?: any\n\n @Field(() => [Int], {\n nullable: true,\n description:\n 'Refids of components currently selected in the modeller (universal numeric handle, things-scene auto-assigned). For \"selected\" / \"선택한\" intent. Note: distinct from `id` which is a data-binding name.'\n })\n selectedRefids?: number[]\n\n @Field(() => [BoardAIMentionPickInput], {\n nullable: true,\n description:\n 'Explicit #mention picks from popup — {token, refid}. User text stays clean (#token only); these picks let server resolve deterministically without exposing refid in the user-visible message. Tokens not in this list fall back to free name/id/type matching.'\n })\n mentions?: BoardAIMentionPickInput[]\n\n @Field({\n nullable: true,\n description: 'AI model identifier override (e.g. \"claude-opus-4-7\"). Falls back to the configured default when omitted.'\n })\n model?: string\n\n @Field({\n nullable: true,\n description: 'Sampling temperature override. Falls back to assistant default when omitted.'\n })\n temperature?: number\n\n @Field({\n nullable: true,\n description: 'Max output tokens override per LLM call. Falls back to assistant default when omitted.'\n })\n maxTokens?: number\n}\n\n@ObjectType()\nclass BoardAIChatOutput {\n @Field({ description: 'Conversational reply.' })\n reply: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'SceneEditPatch.' })\n patch?: any\n\n @Field({ nullable: true, description: 'Clarifying question when input is ambiguous.' })\n followUp?: string\n\n @Field({ description: 'AI client identifier (provider:model).' })\n clientId: string\n\n @Field({ nullable: true, description: 'Echo of session id (when persisted).' })\n sessionId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the user input.' })\n userMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted ChatMessage id of the AI reply.' })\n assistantMessageId?: string\n\n @Field({ nullable: true, description: 'Persisted PatchEntry id (when patch was generated).' })\n patchId?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usages collected during agentic loop — sequence of {name, arguments, result, kind}. UI fold-able box for transparency / debug.'\n })\n toolUsages?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Ephemeral scene actions (selection/view/mode) — sequence of SceneActionOp. Applied via scene-action-execute event on the host. Distinct from `patch` which carries persistent model changes.'\n })\n actions?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Reported when the stored history exceeded the prompt cap for this turn: { omitted, summarized }. The surface uses it to tell the user the conversation grew long — a new conversation is better for a different topic. Objective fact only; no topic-change detection.'\n })\n historyFolded?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Action proposals the AI recorded without executing — each carries what a tool returned alongside `proposed: true`. The surface renders a confirm button; execution is the user\\'s act, never the assistant\\'s. Null when nothing was proposed.'\n })\n proposals?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Ungrounded identifiers mentioned by the reply — present in the answer but absent from everything the model received (prompt, board context, history, tool results). Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'\n })\n groundingWarnings?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Grounded analysis contract for this turn: scope, evidence-backed claims, limitations, proposal and explicit approval boundary. Null when the turn did not perform an analysis.'\n })\n analysis?: any\n}\n\n@Resolver()\nexport class BoardAIChatResolver {\n /**\n * NOTE: 일부러 @Directive('@transaction') 사용하지 않는다.\n * LLM 호출은 수 초~수십 초 — 트랜잭션 안에서 호출하면 SQLite write lock 을\n * 그 시간 동안 보유 → 다른 작업 (cache 정리 등) 과 SQLITE_BUSY 충돌.\n * 따라서 DB 쓰기를 LLM 호출 전후의 짧은 단위로 나누고 각각 auto-commit.\n * 데이터 일관성: 채팅 이력은 부분 누락보다 부분 보존이 안전 (대화 컨텍스트).\n */\n @Mutation(() => BoardAIChatOutput, {\n description: 'AI 주도 보드 모델링 — 자연어 채팅으로 보드 생성·구조편집·스타일링. sessionId 로 영속 컨텍스트.'\n })\n /*\n * ── 이 문은 「묻는 것」이지 「보드를 고치는 것」이 아니다 (2026-09-10) ──────────\n * 공장 현황을 묻는 화면이 **보드 편집 권한**을 요구하던 것이 이 분리의 시작이었다. 대화는\n * `ai-assistant:mutation`, 보드 편집은 `board-ai:mutation` — 둘은 다른 능력이다.\n *\n * ⚠ 문만 바꾸면 **거꾸로 넓어진다.** 코어 보드 편집 도구가 기본으로 켜져 있으므로, 대화 권한만\n * 가진 사람이 이 문으로 보드를 고치게 된다. 그래서 아래에서 **보드 편집 권한을 따로 보고**\n * 없으면 그 도구를 끈다(§`effectiveBoardTools`).\n */\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\")')\n async boardAIChat(\n @Arg('input') input: BoardAIChatInput,\n @Ctx() context: ResolverContext\n ): Promise<BoardAIChatOutput> {\n const base = resolveAIClient()\n if (!base) {\n throw new Error(\n 'No AI client configured. Set config.aiClient = { provider: \"anthropic\" | \"gemini\", model, apiKey } or call setDefaultAIClient(...).'\n )\n }\n\n const { domain, user } = context.state\n const sessionRepo = getRepository(ChatSession)\n const messageRepo = getRepository(ChatMessage)\n const patchRepo = getRepository(PatchEntry)\n\n // ── Phase A: LLM 호출 전 짧은 DB 쓰기 ─────────────────────────\n let session: ChatSession | undefined\n if (input.sessionId) {\n const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } })\n if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)\n session = found\n }\n\n const lastUserMsg = input.messages[input.messages.length - 1]\n let userMessageId: string | undefined\n if (session && lastUserMsg && lastUserMsg.role === 'user') {\n // popup pick 의 refid 를 #token{refid:N} inline marker 로 content 에 박아 저장.\n // 단일 source-of-truth — 별도 컬럼 없이 content 안에서 token-refid 매핑 보존.\n const enriched = injectMentionRefids(lastUserMsg.content, input.mentions)\n // parentMessage = 이 세션의 직전 메시지 (보통 assistant / system / 또는 첫 turn 이면 null).\n // tree 구조 인프라 — branching (regenerate / 사용자별 분기) 는 같은 부모 child 로.\n const previousLast = await messageRepo.findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const saved = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'user',\n content: enriched,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n })\n userMessageId = saved.id\n\n /* 세션 활동 갱신(비정규화) — 목록 최근활동순 정렬과 다른 참여자의 안 읽음 판정 기준.\n * 이름이 아직 없으면 **첫 발화로 자동 작명** 한다 — `세션 2` 보다 \"A-DC 포화 원인\" 이 목록에서\n * 훨씬 유용하다. 사용자가 직접 바꾼 이름은 여기서 건드리지 않는다(이름이 있으면 그대로).\n * best-effort: 실패해도 대화를 막지 않는다(목록 정렬이 한 칸 늦을 뿐). */\n try {\n const patch: any = activityPatch(enriched)\n if (!session.name) patch.name = autoSessionName(enriched)\n await sessionRepo.update(session.id!, patch)\n } catch {\n /* noop */\n }\n /* 내가 참여한 세션 활동 방송 — 창을 닫아 둔 다른 참여자의 배지·목록을 위해. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enriched,\n actorId: user?.id\n })\n\n // C4: 사용자 메시지를 같은 세션 구독자에게 방송 (그룹챗 실시간). best-effort.\n // 자기 발신 에코는 클라이언트가 message.id 로 dedup.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved })\n\n // C4: @user 멘션 → 세션 참여자(member) 자동 등록 (초대). best-effort — 실패해도 chat 흐름 무영향.\n // enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자/자기 제외한 신규만 member 로 등록.\n try {\n await this._inviteMentionedUsers(enriched, session.id!, domain.id, user, session.boardId, (context.state as any).notify)\n } catch {\n /* 초대 실패는 대화를 막지 않는다 */\n }\n }\n\n // ── Phase B: LLM 호출 (DB 잠금 없음, 오래 걸려도 OK) ───────────\n const assistant = new DefaultBoardAIAssistant(base, {\n scopes: input.scopes,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined\n })\n\n /* 에이전트가 볼 이력 — **세션이 있으면 서버 정본(ChatMessage)** 을 쓴다.\n * 클라이언트가 보낸 줄을 쓰면 협의에서 참여자마다 화면이 달라 에이전트가 사람마다 다른 대화를 본다.\n * 발신자 표기·system 기록 포함·접은 구간 제외 규칙은 buildLlmHistory 가 소유(그 파일 머리말 참조).\n * 즉석 대화(sessionId 없음)는 영속 이력이 없으므로 클라이언트 이력을 그대로 쓴다.\n *\n * 정렬은 createdAt ASC + id ASC — 같은 시각 삽입의 순서까지 결정적으로 만든다(조립 쪽에서\n * 다시 정렬하지 않으므로 여기서 확정해야 한다). 멘션 마커는 여기서 제거해 넘긴다. */\n let llmMessages: LLMMessage[]\n let historyFolded: { omitted: number; summarized: boolean } | undefined\n if (session) {\n const rows = await messageRepo.find({\n where: { session: { id: session.id } as any },\n relations: { creator: true },\n order: { createdAt: 'ASC', id: 'ASC' }\n })\n /* 이번 턴에 상한 밖으로 밀려난 것이 있는가 — 화면이 \"대화가 길어졌다\" 를 알릴 근거(사실만). */\n const capped = splitByCap(\n rows.map(row => ({ id: row.id, role: row.role, content: row.content ?? '' })),\n LLM_HISTORY_MAX_TURNS\n )\n if (capped.dropped.length > 0) {\n historyFolded = { omitted: capped.dropped.length, summarized: !!(session as any).lastSummary }\n }\n llmMessages = buildLlmHistory(\n rows.map(row => ({\n id: row.id,\n role: row.role,\n content: stripMentionRefids(row.content ?? ''),\n senderId: (row as any).creator?.id,\n /* 표시 이름만 — 이메일 등 개인정보는 프롬프트에 넣지 않는다. */\n senderName: (row as any).creator?.name || (row as any).creator?.username\n })),\n {\n truncateAfterMessageId: input.truncateAfterMessageId ?? undefined,\n /* 방금 저장한 사용자 메시지는 접은 구간보다 뒤에 있어도 반드시 남긴다. */\n keepMessageIds: userMessageId ? [userMessageId] : [],\n /* 이력 상한 — 협의는 며칠 이어진다. 상한이 없으면 매 턴 전체를 보내 프롬프트가 무한히 자란다. */\n maxTurns: LLM_HISTORY_MAX_TURNS,\n /* 밀려난 앞부분은 요약으로 대체한다(있으면). 요약은 응답 뒤에 갱신한다 — 아래 Phase D. */\n summary: { text: (session as any).lastSummary, upToMessageId: (session as any).summaryUpToMessageId }\n }\n )\n } else {\n llmMessages = input.messages.map(m => ({\n role: m.role === 'assistant' ? 'assistant' : 'user',\n content: stripMentionRefids(m.content)\n }))\n }\n\n const r = await assistant.chat(llmMessages, input.currentBoard ?? undefined, {\n scopes: input.scopes,\n hostContext: input.hostContext ?? undefined,\n /* 대화면별 도구 노출 — 미지정이면 현행(전부 허용) 그대로. */\n toolCategories: input.toolCategories ?? undefined,\n boardTools: await effectiveBoardTools(input.boardTools, context),\n requireGroundingTools: input.requireGroundingTools ?? undefined,\n knownTypes: input.knownTypes,\n categories: input.categories as ComponentCategory[] | undefined,\n selectedRefids: input.selectedRefids,\n mentionPicks: input.mentions,\n componentSchemas: Array.isArray(input.componentSchemas) ? input.componentSchemas : undefined,\n model: input.model,\n temperature: input.temperature ?? undefined,\n maxTokens: input.maxTokens ?? undefined,\n // 외부 도메인 (board-import 등) 의 read/external tool builder 가 server-side\n // 의존성 (도메인 / 유저 / 트랜잭션) 을 사용할 수 있도록 ResolverContext.state 전달.\n // board-ai 코어 자체는 이 값을 알지 않고 dispatch 단계에서 closure 로만 흘림.\n toolCallContext: context.state\n })\n\n // ── Phase C: LLM 호출 후 짧은 DB 쓰기 ─────────────────────────\n let assistantMessageId: string | undefined\n let patchId: string | undefined\n if (session) {\n let patchEntry: PatchEntry | undefined\n if (r.patch) {\n patchEntry = await patchRepo.save({\n session: { id: session.id } as any,\n source: 'ai',\n ops: JSON.stringify(r.patch.ops),\n summary: r.patch.summary,\n confidence: r.patch.confidence,\n reverted: false\n })\n patchId = patchEntry.id\n // C1: 같은 board 구독자에게 방송 (실시간 공동편집 spine). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: patchEntry })\n }\n\n // AI 응답의 #token 들을 currentBoard 에 대해 resolve 한 뒤 inline marker 로 박음.\n // patch 로 새로 만든 컴포넌트는 input.currentBoard 에 없으므로 marker 없이 저장 →\n // 클릭 시 runtime fallback 으로 해석 (그때는 patch 적용 후라 scene 에 존재).\n const enrichedReply = injectAiMentionRefids(r.reply, input.currentBoard ?? null)\n\n const aiMsg = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'assistant',\n content: enrichedReply,\n relatedPatchId: patchEntry?.id,\n toolUsages: r.toolUsages && r.toolUsages.length > 0 ? JSON.stringify(r.toolUsages) : undefined,\n /* 접지 경고를 함께 영속 — 나중에 읽는 참여자도 같은 주의를 봐야 한다(재계산 불가: 근거 원문이 남지 않는다).\n * 컬럼 길이(1024) 안에 들도록 앞 20개까지만 — 그 이상이면 목록이 아니라 답 자체가 문제다. */\n groundingWarnings:\n r.groundingWarnings && r.groundingWarnings.length > 0\n ? JSON.stringify(r.groundingWarnings.slice(0, 20))\n : undefined,\n // assistant 메시지의 creator = 이 응답을 트리거한 user (= 직전 user 메시지의 작성자).\n // 같은 turn 으로 묶임 → 향후 사용자별 필터에서 user/assistant 가 함께 따라옴.\n creator: user,\n updater: user,\n // parentMessage = 이 응답이 답하는 user 메시지. regenerate 시 같은 user 메시지를\n // 부모로 하는 형제 assistant 들이 생기면 자연스럽게 branching tree.\n parentMessage: userMessageId ? ({ id: userMessageId } as any) : undefined\n })\n assistantMessageId = aiMsg.id\n\n // C4: AI 응답도 같은 세션 구독자에게 방송 (트리거한 본인 포함 — id dedup). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: aiMsg })\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\" 으로 보인다. */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: enrichedReply,\n actorId: user?.id\n })\n\n await sessionRepo.update(session.id, {\n aiClientId: base.id,\n updater: user,\n /* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 \"새 소식 없음\"으로 보인다. */\n ...activityPatch(enrichedReply)\n })\n\n /* ── Phase D: 이력 요약 접기 ────────────────────────────────────────────\n * 응답을 **돌려준 뒤** 갱신한다 — 사용자 대기 시간에 요약용 LLM 호출을 얹지 않는다.\n * 기다리지 않으므로(await 없음) 실패해도 대화에는 영향이 없다: 요약이 없으면 다음 턴은\n * \"N개 생략\" 으로 정직하게 알리고 넘어간다(조용한 맥락 상실이 아니다). */\n void this._foldHistorySummary(session.id!, domain.id, base)\n }\n\n return {\n reply: r.reply,\n patch: r.patch ?? null,\n followUp: r.followUp ?? null,\n clientId: base.id,\n sessionId: session?.id,\n userMessageId,\n assistantMessageId,\n patchId,\n toolUsages: r.toolUsages ?? null,\n actions: r.actions && r.actions.length > 0 ? r.actions : null,\n proposals: r.proposals && r.proposals.length > 0 ? r.proposals : null,\n historyFolded: historyFolded ?? null,\n groundingWarnings: r.groundingWarnings && r.groundingWarnings.length > 0 ? r.groundingWarnings : null,\n analysis: r.analysis ?? null\n }\n }\n\n /**\n * 이력 요약 접기 — 상한 밖으로 밀려난 앞부분을 요약으로 눌러 담는다.\n *\n * 왜 이렇게 나눠 두었나: 판정(무엇을 요약할지·다시 만들 때인지)은 순수 로직(history-summary)이\n * 갖고, 여기서는 **호출과 저장**만 한다. 그래야 규칙을 테스트로 고정할 수 있다.\n *\n * 아껴 만든다: 새로 밀려난 것이 임계치를 넘을 때만 LLM 을 부른다(상한을 넘긴 뒤 매 턴 부르면\n * 대화마다 호출이 두 번씩 붙는다). 실패는 조용히 넘긴다 — 다음 턴이 \"생략\" 으로 알리고,\n * 다음 임계치에서 다시 시도한다.\n */\n private async _foldHistorySummary(\n sessionId: string,\n domainId: string,\n client: AIClient\n ): Promise<void> {\n try {\n const sessionRepo = getRepository(ChatSession)\n const session = await sessionRepo.findOneBy({ id: sessionId, domain: { id: domainId } as any })\n if (!session) return\n\n const stored = {\n text: (session as any).lastSummary,\n upToMessageId: (session as any).summaryUpToMessageId\n }\n const { rows, startsAfterSummary } = await this._historyForFold(sessionId, stored)\n const history = rows.map(row => ({\n id: row.id,\n role: row.role,\n content: stripMentionRefids(row.content ?? ''),\n senderId: (row as any).creator?.id,\n senderName: (row as any).creator?.name || (row as any).creator?.username\n }))\n const { patch } = await foldHistorySummary({\n rows: history,\n rowsStartAfterSummary: startsAfterSummary,\n stored,\n maxTurns: LLM_HISTORY_MAX_TURNS,\n threshold: LLM_SUMMARY_THRESHOLD,\n summarize: content =>\n client.chat([{ role: 'user', content }], {\n systemPrompt: summaryInstruction(),\n maxTokens: 700,\n /* 요약은 창작이 아니다 — 흔들림을 줄인다. */\n temperature: 0\n })\n })\n if (patch) await sessionRepo.update(sessionId, patch as any)\n } catch {\n /* 요약 실패는 대화를 막지 않는다 — 다음 턴은 \"생략\" 으로 정직하게 알린다. */\n }\n }\n\n /**\n * 요약에 필요한 만큼만 읽는다 — **요약이 덮은 지점 뒤부터.**\n *\n * ── 왜 (2026-08-20) ─────────────────────────────────────────────────────────\n * 이 함수를 부르는 자리는 **메시지를 보낼 때마다**다. 예전에는 세션의 대화를 처음부터 끝까지 읽었고\n * (`creator` 조인까지), 정작 쓰는 것은 두 조각뿐이었다: 마지막 `maxTurns`개와, 요약 워터마크 뒤부터의\n * 미반영분. 대화가 2,000개면 매 메시지마다 2,000행을 읽어 수십 행을 쓴 셈이다.\n *\n * 워터마크 뒤부터 읽으면 그 구간이 곧 `미반영분 + 최근 유지분`이라, 읽는 행 수가 **대화 길이와\n * 무관**해진다(정상 상태에서 대략 `threshold + maxTurns`).\n *\n * ── 안전 규율을 지킨다 ──────────────────────────────────────────────────────\n * 워터마크 행이 지워졌거나(삭제·편집) 요약이 아직 없으면 **전체를 읽는다.** 그때는 요약이 어디까지\n * 덮는지 알 수 없고, 「빠뜨리는 쪽보다 다시 요약하는 쪽이 안전하다」는 것이 이 기능의 규율이다\n * (`pendingForSummary` 주석). 그 경우는 드물다.\n *\n * 순서는 `(createdAt, id)` — 읽는 순서와 자르는 기준이 같아야 한다. 같은 시각의 메시지가 있으면\n * id 로 가른다(원래 정렬이 이미 그 규약이다).\n */\n private async _historyForFold(\n sessionId: string,\n stored: { text?: string | null; upToMessageId?: string | null }\n ): Promise<{ rows: ChatMessage[]; startsAfterSummary: boolean }> {\n const repo = getRepository(ChatMessage)\n const session = { id: sessionId } as any\n const order = { createdAt: 'ASC', id: 'ASC' } as any\n const relations = { creator: true } as any\n\n if (stored.text && stored.upToMessageId) {\n const mark = await repo.findOne({ where: { session, id: stored.upToMessageId } })\n if (mark?.createdAt) {\n const rows = await repo.find({\n /* 배열 = OR. 같은 시각 안에서는 id 로 이어 자른다(정렬 규약과 같은 축). */\n where: [\n { session, createdAt: MoreThan(mark.createdAt) },\n { session, createdAt: mark.createdAt, id: MoreThan(mark.id) }\n ],\n relations,\n order\n })\n return { rows, startsAfterSummary: true }\n }\n }\n\n /* 워터마크가 없거나 그 행이 사라졌다 — 덮은 범위를 모르므로 전부 다시 본다. */\n return { rows: await repo.find({ where: { session }, relations, order }), startsAfterSummary: false }\n }\n\n /**\n * C4: 메시지에 @멘션된 사용자를 세션 참여자(member)로 자동 등록 — 초대.\n * enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자·자기 제외한 신규만 등록.\n * 소프트삭제(deletedAt)된 과거 참여자는 다시 살린다(재초대). 신규 등록 후 협업 초대 알림 발송(C4b-2, best-effort).\n */\n private async _inviteMentionedUsers(\n content: string,\n sessionId: string,\n domainId: string,\n inviter: any,\n boardId?: string,\n notify?: (arg: any) => Promise<void>\n ): Promise<void> {\n const mentioned = extractMentionUserIds(content)\n if (mentioned.length === 0) return\n\n const repo = getRepository(ChatSessionParticipant)\n const existing = await repo.find({\n where: { session: { id: sessionId } as any, domain: { id: domainId } as any },\n withDeleted: true\n })\n const activeIds = existing.filter(p => !p.deletedAt).map(p => p.userId!).filter(Boolean)\n const fresh = newParticipantUserIds(mentioned, activeIds, inviter?.id)\n if (fresh.length === 0) return\n\n for (const userId of fresh) {\n // 과거에 나갔던(soft-deleted) 참여자면 되살리고, 아니면 새로 등록.\n const prior = existing.find(p => p.userId === userId && p.deletedAt)\n if (prior) {\n await repo.update(prior.id!, { deletedAt: null as any, role: 'member', updater: inviter } as any)\n } else {\n await repo.save({\n domain: { id: domainId } as any,\n session: { id: sessionId } as any,\n user: { id: userId } as any,\n role: 'member',\n creator: inviter,\n updater: inviter\n } as any)\n }\n }\n\n // C4b-2: 초대 알림 — notification 모듈의 기존 발송 메커니즘 재사용(중복 구현 금지).\n // context.state.notify 는 notification-middleware 가 전역 주입(FCM/pubsub). duck-typed →\n // board-ai 에 의존성 추가 없이 런타임 호출. 미로드 환경이면 undefined → graceful skip.\n this._notifyInvited(notify, fresh, boardId, inviter)\n }\n\n /** 초대된 사용자들에게 협업 초대 알림 발송 (best-effort, capability 있을 때만). */\n private _notifyInvited(\n notify: ((arg: any) => Promise<void>) | undefined,\n userIds: string[],\n boardId: string | undefined,\n inviter: any\n ): void {\n if (typeof notify !== 'function') return\n const payload = buildInviteNotification(userIds, boardId, inviter)\n if (!payload) return\n Promise.resolve(notify(payload)).catch(() => {\n /* 알림 실패는 초대·대화를 막지 않는다 */\n })\n }\n}\n\nfunction resolveAIClient(): AIClient | undefined {\n const existing = getDefaultAIClient()\n if (existing) return existing\n const cfg = config.get('aiClient', null) as any\n if (cfg && cfg.provider) {\n return createAIClient(cfg)\n }\n return undefined\n}\n"]}
@@ -4,7 +4,6 @@ import { BoardSessionResolver } from './board-session-resolver.js';
4
4
  import { BoardPatchSubscription } from './patch-entry/board-patch-subscription.js';
5
5
  export * from './types.js';
6
6
  export * from './assistant.js';
7
- export * from './apply-patch.js';
8
7
  export * from './prefab-registry.js';
9
8
  export * from './mention-resolver.js';
10
9
  export * from './catalog-resolver.js';
@@ -18,7 +18,6 @@ const board_patch_subscription_js_1 = require("./patch-entry/board-patch-subscri
18
18
  const index_js_1 = require("./patch-entry/index.js");
19
19
  tslib_1.__exportStar(require("./types.js"), exports);
20
20
  tslib_1.__exportStar(require("./assistant.js"), exports);
21
- tslib_1.__exportStar(require("./apply-patch.js"), exports);
22
21
  tslib_1.__exportStar(require("./prefab-registry.js"), exports);
23
22
  tslib_1.__exportStar(require("./mention-resolver.js"), exports);
24
23
  tslib_1.__exportStar(require("./catalog-resolver.js"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;GAQG;AACH,uEAA4C;AAE5C,iEAA4D;AAC5D,2EAAkE;AAClE,2FAAkF;AAClF,qDAAuE;AAEvE,qDAA0B;AAC1B,yDAA8B;AAC9B,2DAAgC;AAChC,+DAAoC;AACpC,gEAAqC;AACrC,gEAAqC;AACrC,iEAAsC;AACtC,iEAAsC;AAEtC,6DAA6D;AAChD,QAAA,QAAQ,GAAG,CAAC,GAAG,mBAAkB,CAAC,CAAA;AAElC,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,0CAAmB;QACnB,gDAAgD;QAChD,gDAAoB;QACpB,oDAAsB;KACvB;CACF,CAAA","sourcesContent":["/*\n * 대화 기계는 **`@things-factory/ai-assistant` 로 나갔다** (2026-09-10).\n *\n * 세션·메시지·참가자·인박스·구독은 보드가 없는 제품에도 뜻이 있다. 여기 남은 것은 보드 저작이다.\n * 아래 재수출은 이 패키지를 지나는 소비처(아홉 앱)가 이름을 그대로 쓰게 한다 — 베낀 것이 아니라\n * 가리키는 것이다.\n *\n * 자르는 선과 이유는 `things-factory/docs/design/ai-modules.md`.\n */\nexport * from '@things-factory/ai-assistant'\n\nimport { BoardAIChatResolver } from './board-ai-resolver.js'\nimport { BoardSessionResolver } from './board-session-resolver.js'\nimport { BoardPatchSubscription } from './patch-entry/board-patch-subscription.js'\nimport { entities as PatchEntryEntities } from './patch-entry/index.js'\n\nexport * from './types.js'\nexport * from './assistant.js'\nexport * from './apply-patch.js'\nexport * from './prefab-registry.js'\nexport * from './mention-resolver.js'\nexport * from './catalog-resolver.js'\nexport * from './board-ai-resolver.js'\nexport * from './patch-entry/index.js'\n\n/* 세션 계열 엔티티는 `ai-assistant` 가 낸다 — 프레임워크가 의존을 타고 재귀로 모은다. */\nexport const entities = [...PatchEntryEntities]\n\nexport const schema = {\n resolverClasses: [\n BoardAIChatResolver,\n /* 보드에 매인 op 여섯 — 중립 세션 op 과 갈랐다(2026-09-10). */\n BoardSessionResolver,\n BoardPatchSubscription,\n ]\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;GAQG;AACH,uEAA4C;AAE5C,iEAA4D;AAC5D,2EAAkE;AAClE,2FAAkF;AAClF,qDAAuE;AAEvE,qDAA0B;AAC1B,yDAA8B;AAC9B,+DAAoC;AACpC,gEAAqC;AACrC,gEAAqC;AACrC,iEAAsC;AACtC,iEAAsC;AAEtC,6DAA6D;AAChD,QAAA,QAAQ,GAAG,CAAC,GAAG,mBAAkB,CAAC,CAAA;AAElC,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,0CAAmB;QACnB,gDAAgD;QAChD,gDAAoB;QACpB,oDAAsB;KACvB;CACF,CAAA","sourcesContent":["/*\n * 대화 기계는 **`@things-factory/ai-assistant` 로 나갔다** (2026-09-10).\n *\n * 세션·메시지·참가자·인박스·구독은 보드가 없는 제품에도 뜻이 있다. 여기 남은 것은 보드 저작이다.\n * 아래 재수출은 이 패키지를 지나는 소비처(아홉 앱)가 이름을 그대로 쓰게 한다 — 베낀 것이 아니라\n * 가리키는 것이다.\n *\n * 자르는 선과 이유는 `things-factory/docs/design/ai-modules.md`.\n */\nexport * from '@things-factory/ai-assistant'\n\nimport { BoardAIChatResolver } from './board-ai-resolver.js'\nimport { BoardSessionResolver } from './board-session-resolver.js'\nimport { BoardPatchSubscription } from './patch-entry/board-patch-subscription.js'\nimport { entities as PatchEntryEntities } from './patch-entry/index.js'\n\nexport * from './types.js'\nexport * from './assistant.js'\nexport * from './prefab-registry.js'\nexport * from './mention-resolver.js'\nexport * from './catalog-resolver.js'\nexport * from './board-ai-resolver.js'\nexport * from './patch-entry/index.js'\n\n/* 세션 계열 엔티티는 `ai-assistant` 가 낸다 — 프레임워크가 의존을 타고 재귀로 모은다. */\nexport const entities = [...PatchEntryEntities]\n\nexport const schema = {\n resolverClasses: [\n BoardAIChatResolver,\n /* 보드에 매인 op 여섯 — 중립 세션 op 과 갈랐다(2026-09-10). */\n BoardSessionResolver,\n BoardPatchSubscription,\n ]\n}\n"]}
@@ -7,7 +7,7 @@ const tslib_1 = require("tslib");
7
7
  * BoardPatchSubscription (Phase Board-AI C1 — op-브로드캐스트 spine).
8
8
  *
9
9
  * PatchEntry(AI 또는 user-direct 편집) 가 생성될 때마다 같은 board 구독자에게 방송 →
10
- * 실시간 공동편집의 척추. 클라이언트(C2)가 구독해 원격 patch 를 applyBoardEditPatch 로 적용.
10
+ * 실시간 공동편집의 척추. 클라이언트(C2)가 구독해 원격 patch 를 applyScenePatch 로 적용.
11
11
  *
12
12
  * 전송/패턴은 기존 board-service/board-subscription.ts 복제 (PubSub + graphql-ws, domain 격리).
13
13
  * 순수 로직(토픽/필터)은 board-patch-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.
@@ -1 +1 @@
1
- {"version":3,"file":"board-patch-subscription.js","sourceRoot":"","sources":["../../../server/service/patch-entry/board-patch-subscription.ts"],"names":[],"mappings":";;;AAuBA,8CAOC;;AA9BD;;;;;;;;GAQG;AACH,+CAAgE;AAChE,+CAA2C;AAC3C,iDAA8C;AAC9C,qDAA6C;AAC7C,yEAAwG;AACxG,+DAA2E;AAE3E,uEAAgF;AAAvE,6HAAA,iBAAiB,OAAA;AAAE,4HAAA,gBAAgB,OAAA;AAG5C;;;GAGG;AACH,SAAgB,iBAAiB,CAAC,OAA0B;IAC1D,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAM;IAC5B,IAAI,CAAC;QACH,cAAM,CAAC,OAAO,CAAC,4CAAiB,EAAE,OAAO,CAAC,CAAA;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,uBAAuB;IACzB,CAAC;AACH,CAAC;AAGM,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAajC,kBAAkB,CAAS,OAA0B,EAAkB,OAAe;QACpF,OAAO,OAAO,CAAC,KAAK,CAAA;IACtB,CAAC;CACF,CAAA;AAhBY,wDAAsB;AAajC;IAZC,IAAA,2BAAY,EAAC;QACZ,WAAW,EACT,8FAA8F;QAChG,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;YAExB,MAAM,IAAA,2CAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,4CAAiB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,2CAAgB,EAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;QAChG,CAAC;KACF,CAAC;IACkB,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA8B,mBAAA,IAAA,kBAAG,EAAC,SAAS,CAAC,CAAA;;;4CAAmB,2BAAU;gEAElG;iCAfU,sBAAsB;IADlC,IAAA,uBAAQ,EAAC,2BAAU,CAAC;GACR,sBAAsB,CAgBlC","sourcesContent":["/*\n * BoardPatchSubscription (Phase Board-AI C1 — op-브로드캐스트 spine).\n *\n * PatchEntry(AI 또는 user-direct 편집) 가 생성될 때마다 같은 board 구독자에게 방송 →\n * 실시간 공동편집의 척추. 클라이언트(C2)가 구독해 원격 patch 를 applyBoardEditPatch 로 적용.\n *\n * 전송/패턴은 기존 board-service/board-subscription.ts 복제 (PubSub + graphql-ws, domain 격리).\n * 순수 로직(토픽/필터)은 board-patch-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.\n */\nimport { Resolver, Subscription, Root, Arg } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\nimport { PatchEntry } from './patch-entry.js'\nimport { BOARD_PATCH_TOPIC, boardPatchFilter, type BoardPatchPayload } from './board-patch-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '@things-factory/ai-assistant'\n\nexport { BOARD_PATCH_TOPIC, boardPatchFilter } from './board-patch-broadcast.js'\nexport type { BoardPatchPayload } from './board-patch-broadcast.js'\n\n/**\n * PatchEntry 생성 직후 호출 — 같은 board 구독자에게 방송.\n * boardId 없으면(세션 미연결) skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.\n */\nexport function publishBoardPatch(payload: BoardPatchPayload): void {\n if (!payload.boardId) return\n try {\n pubsub.publish(BOARD_PATCH_TOPIC, payload)\n } catch {\n /* 방송 생략 — 영속은 이미 끝남 */\n }\n}\n\n@Resolver(PatchEntry)\nexport class BoardPatchSubscription {\n @Subscription({\n description:\n 'Subscribes to board edit patches (AI or user-direct) for real-time collaboration on a board.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { boardId } = args\n\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n return pipe(pubsub.subscribe(BOARD_PATCH_TOPIC), filter(boardPatchFilter(domain.id, boardId)))\n }\n })\n boardPatchActivity(@Root() payload: BoardPatchPayload, @Arg('boardId') boardId: string): PatchEntry {\n return payload.patch\n }\n}\n"]}
1
+ {"version":3,"file":"board-patch-subscription.js","sourceRoot":"","sources":["../../../server/service/patch-entry/board-patch-subscription.ts"],"names":[],"mappings":";;;AAuBA,8CAOC;;AA9BD;;;;;;;;GAQG;AACH,+CAAgE;AAChE,+CAA2C;AAC3C,iDAA8C;AAC9C,qDAA6C;AAC7C,yEAAwG;AACxG,+DAA2E;AAE3E,uEAAgF;AAAvE,6HAAA,iBAAiB,OAAA;AAAE,4HAAA,gBAAgB,OAAA;AAG5C;;;GAGG;AACH,SAAgB,iBAAiB,CAAC,OAA0B;IAC1D,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAM;IAC5B,IAAI,CAAC;QACH,cAAM,CAAC,OAAO,CAAC,4CAAiB,EAAE,OAAO,CAAC,CAAA;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,uBAAuB;IACzB,CAAC;AACH,CAAC;AAGM,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAajC,kBAAkB,CAAS,OAA0B,EAAkB,OAAe;QACpF,OAAO,OAAO,CAAC,KAAK,CAAA;IACtB,CAAC;CACF,CAAA;AAhBY,wDAAsB;AAajC;IAZC,IAAA,2BAAY,EAAC;QACZ,WAAW,EACT,8FAA8F;QAChG,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;YAExB,MAAM,IAAA,2CAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,4CAAiB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,2CAAgB,EAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;QAChG,CAAC;KACF,CAAC;IACkB,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA8B,mBAAA,IAAA,kBAAG,EAAC,SAAS,CAAC,CAAA;;;4CAAmB,2BAAU;gEAElG;iCAfU,sBAAsB;IADlC,IAAA,uBAAQ,EAAC,2BAAU,CAAC;GACR,sBAAsB,CAgBlC","sourcesContent":["/*\n * BoardPatchSubscription (Phase Board-AI C1 — op-브로드캐스트 spine).\n *\n * PatchEntry(AI 또는 user-direct 편집) 가 생성될 때마다 같은 board 구독자에게 방송 →\n * 실시간 공동편집의 척추. 클라이언트(C2)가 구독해 원격 patch 를 applyScenePatch 로 적용.\n *\n * 전송/패턴은 기존 board-service/board-subscription.ts 복제 (PubSub + graphql-ws, domain 격리).\n * 순수 로직(토픽/필터)은 board-patch-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.\n */\nimport { Resolver, Subscription, Root, Arg } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\nimport { PatchEntry } from './patch-entry.js'\nimport { BOARD_PATCH_TOPIC, boardPatchFilter, type BoardPatchPayload } from './board-patch-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '@things-factory/ai-assistant'\n\nexport { BOARD_PATCH_TOPIC, boardPatchFilter } from './board-patch-broadcast.js'\nexport type { BoardPatchPayload } from './board-patch-broadcast.js'\n\n/**\n * PatchEntry 생성 직후 호출 — 같은 board 구독자에게 방송.\n * boardId 없으면(세션 미연결) skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.\n */\nexport function publishBoardPatch(payload: BoardPatchPayload): void {\n if (!payload.boardId) return\n try {\n pubsub.publish(BOARD_PATCH_TOPIC, payload)\n } catch {\n /* 방송 생략 — 영속은 이미 끝남 */\n }\n}\n\n@Resolver(PatchEntry)\nexport class BoardPatchSubscription {\n @Subscription({\n description:\n 'Subscribes to board edit patches (AI or user-direct) for real-time collaboration on a board.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { boardId } = args\n\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n return pipe(pubsub.subscribe(BOARD_PATCH_TOPIC), filter(boardPatchFilter(domain.id, boardId)))\n }\n })\n boardPatchActivity(@Root() payload: BoardPatchPayload, @Arg('boardId') boardId: string): PatchEntry {\n return payload.patch\n }\n}\n"]}
@@ -5,7 +5,7 @@ export declare class PatchEntry {
5
5
  sessionId?: string;
6
6
  /** 'ai' | 'user-direct' | 'import' */
7
7
  source: string;
8
- /** BoardEditOp[] JSON-stringified — column 으로는 long text. */
8
+ /** SceneEditOp[] JSON-stringified — column 으로는 long text. */
9
9
  ops: string;
10
10
  /** GraphQL 노출용 — JSON 객체로 변환. */
11
11
  get opsJson(): any;
@@ -64,7 +64,7 @@ tslib_1.__decorate([
64
64
  tslib_1.__metadata("design:type", String)
65
65
  ], PatchEntry.prototype, "ops", void 0);
66
66
  tslib_1.__decorate([
67
- (0, type_graphql_1.Field)(type => graphql_scalars_1.GraphQLJSON, { description: 'BoardEditOp[] (parsed JSON array).' }),
67
+ (0, type_graphql_1.Field)(type => graphql_scalars_1.GraphQLJSON, { description: 'SceneEditOp[] (parsed JSON array).' }),
68
68
  tslib_1.__metadata("design:type", Object),
69
69
  tslib_1.__metadata("design:paramtypes", [])
70
70
  ], PatchEntry.prototype, "opsJson", null);
@@ -1 +1 @@
1
- {"version":3,"file":"patch-entry.js","sourceRoot":"","sources":["../../../server/service/patch-entry/patch-entry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PatchEntry — 보드의 한 변경 단위 (영구 이력).\n *\n * source 별:\n * - 'ai': AI 가 chat 응답에서 생성\n * - 'user-direct': 사용자가 모델러에서 직접 편집 — 변경 이벤트로 자동 기록\n * - 'import': ImportSession 결과 (도면 → 보드)\n *\n * AI 컨텍스트 유지의 핵심: 사용자 직접 편집도 PatchEntry 로 기록되어\n * 다음 chat 호출 시 system 메시지로 자동 합류 → AI 가 모든 변경을 인지.\n */\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId\n} from 'typeorm'\nimport { Field, ID, Float, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { config } from '@things-factory/env'\n\nimport { ChatSession } from '@things-factory/ai-assistant'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_patch_entry_1', (patch: PatchEntry) => [patch.session, patch.createdAt])\n@ObjectType({ description: 'One board edit operation history (cascade-deleted with ChatSession).' })\nexport class PatchEntry {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n /* 역방향(`ChatSession.patches`)을 두지 않는다 — 세션은 중립이고 보드 패치를 몰라야 한다. */\n @ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })\n session?: ChatSession\n\n @RelationId((patch: PatchEntry) => patch.session)\n sessionId?: string\n\n /** 'ai' | 'user-direct' | 'import' */\n @Column({ type: 'varchar', length: 32 })\n @Field({ description: \"'ai' | 'user-direct' | 'import'\" })\n source!: string\n\n /** BoardEditOp[] JSON-stringified — column 으로는 long text. */\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n ops!: string\n\n /** GraphQL 노출용 — JSON 객체로 변환. */\n @Field(type => GraphQLJSON, { description: 'BoardEditOp[] (parsed JSON array).' })\n get opsJson(): any {\n try {\n return JSON.parse(this.ops || '[]')\n } catch {\n return []\n }\n }\n\n @Column({ type: 'text', nullable: true })\n @Field({ nullable: true, description: 'Short human summary of this patch.' })\n summary?: string\n\n @Column({ type: 'float', nullable: true })\n @Field(type => Float, { nullable: true, description: 'AI confidence 0..1 (null for user-direct).' })\n confidence?: number\n\n @Column({ default: false })\n @Field({ description: 'Whether this patch was reverted.' })\n reverted!: boolean\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n}\n"]}
1
+ {"version":3,"file":"patch-entry.js","sourceRoot":"","sources":["../../../server/service/patch-entry/patch-entry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PatchEntry — 보드의 한 변경 단위 (영구 이력).\n *\n * source 별:\n * - 'ai': AI 가 chat 응답에서 생성\n * - 'user-direct': 사용자가 모델러에서 직접 편집 — 변경 이벤트로 자동 기록\n * - 'import': ImportSession 결과 (도면 → 보드)\n *\n * AI 컨텍스트 유지의 핵심: 사용자 직접 편집도 PatchEntry 로 기록되어\n * 다음 chat 호출 시 system 메시지로 자동 합류 → AI 가 모든 변경을 인지.\n */\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId\n} from 'typeorm'\nimport { Field, ID, Float, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { config } from '@things-factory/env'\n\nimport { ChatSession } from '@things-factory/ai-assistant'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_patch_entry_1', (patch: PatchEntry) => [patch.session, patch.createdAt])\n@ObjectType({ description: 'One board edit operation history (cascade-deleted with ChatSession).' })\nexport class PatchEntry {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n /* 역방향(`ChatSession.patches`)을 두지 않는다 — 세션은 중립이고 보드 패치를 몰라야 한다. */\n @ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })\n session?: ChatSession\n\n @RelationId((patch: PatchEntry) => patch.session)\n sessionId?: string\n\n /** 'ai' | 'user-direct' | 'import' */\n @Column({ type: 'varchar', length: 32 })\n @Field({ description: \"'ai' | 'user-direct' | 'import'\" })\n source!: string\n\n /** SceneEditOp[] JSON-stringified — column 으로는 long text. */\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n ops!: string\n\n /** GraphQL 노출용 — JSON 객체로 변환. */\n @Field(type => GraphQLJSON, { description: 'SceneEditOp[] (parsed JSON array).' })\n get opsJson(): any {\n try {\n return JSON.parse(this.ops || '[]')\n } catch {\n return []\n }\n }\n\n @Column({ type: 'text', nullable: true })\n @Field({ nullable: true, description: 'Short human summary of this patch.' })\n summary?: string\n\n @Column({ type: 'float', nullable: true })\n @Field(type => Float, { nullable: true, description: 'AI confidence 0..1 (null for user-direct).' })\n confidence?: number\n\n @Column({ default: false })\n @Field({ description: 'Whether this patch was reverted.' })\n reverted!: boolean\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n}\n"]}
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * preset 적용 전 / 데이터 바인딩 적용 전 깨끗한 상태로 돌리기 위한 helper.
5
5
  */
6
- import type { BoardEditOp } from '../types';
6
+ import type { SceneEditOp } from '@operato/scene-ops';
7
7
  export interface ClearStyleInput {
8
8
  refids: number[];
9
9
  aspects: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity'>;
10
10
  }
11
11
  export interface OpsResult {
12
- ops: BoardEditOp[];
12
+ ops: SceneEditOp[];
13
13
  errors: string[];
14
14
  }
15
15
  export declare function buildClearStyleOps(input: ClearStyleInput): OpsResult;
@@ -1 +1 @@
1
- {"version":3,"file":"clear-tools.js","sourceRoot":"","sources":["../../../server/service/styling/clear-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * clearStyle — 특정 styling aspect 들을 초기화 (제거).\n *\n * preset 적용 전 / 데이터 바인딩 적용 전 깨끗한 상태로 돌리기 위한 helper.\n */\nimport type { BoardEditOp } from '../types'\n\nexport interface ClearStyleInput {\n refids: number[]\n aspects: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity'>\n}\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\nconst ASPECT_TO_PATCH: Record<string, any> = {\n fill: { fillStyle: { type: 'none' } },\n // stroke: strokeStyle 는 색상 문자열 (top-level), 굵기는 lineWidth (top-level).\n // 굵기 0 으로 윤곽선 시각 제거 (색은 유지). 완전 제거는 strokeStyle: null 동시 set.\n stroke: { lineWidth: 0 },\n shadow: { shadow: { blur: 0, color: 'transparent' } },\n material: { material3d: { preset: 'default' } },\n // font: null = delete 시맨틱 (apply-patch.ts:233-243). 핵심 폰트 키 모두 제거 →\n // component 가 기본 폰트 동작으로 복귀. 보존이 의도라면 font aspect 미선택.\n font: {\n fontSize: null,\n fontFamily: null,\n fontColor: null,\n fontWeight: null,\n fontStyle: null\n },\n opacity: { alpha: 1 }\n}\n\nexport function buildClearStyleOps(input: ClearStyleInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n if (!Array.isArray(input.aspects) || input.aspects.length === 0) {\n return { ops: [], errors: ['aspects must be a non-empty array'] }\n }\n const valid = ['fill', 'stroke', 'shadow', 'material', 'font', 'opacity']\n const unknown = input.aspects.filter(a => !valid.includes(a))\n if (unknown.length > 0) {\n return {\n ops: [],\n errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${valid.join('/')}`]\n }\n }\n\n // 모든 aspect 의 patch 를 하나로 병합 (한 refid 당 하나의 modify op)\n const merged: any = {}\n for (const a of input.aspects) {\n Object.assign(merged, ASPECT_TO_PATCH[a])\n }\n if (Object.keys(merged).length === 0) {\n return { ops: [], errors: ['No effective patch — try other aspects'] }\n }\n\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch: merged })),\n errors: []\n }\n}\n"]}
1
+ {"version":3,"file":"clear-tools.js","sourceRoot":"","sources":["../../../server/service/styling/clear-tools.ts"],"names":[],"mappings":";;AAoCA,gDAgCC;AAnDD,MAAM,eAAe,GAAwB;IAC3C,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IACrC,uEAAuE;IACvE,8DAA8D;IAC9D,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE;IACxB,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;IACrD,QAAQ,EAAE,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;IAC/C,oEAAoE;IACpE,uDAAuD;IACvD,IAAI,EAAE;QACJ,QAAQ,EAAE,IAAI;QACd,UAAU,EAAE,IAAI;QAChB,SAAS,EAAE,IAAI;QACf,UAAU,EAAE,IAAI;QAChB,SAAS,EAAE,IAAI;KAChB;IACD,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE;CACtB,CAAA;AAED,SAAgB,kBAAkB,CAAC,KAAsB;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAA;IAClE,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,mCAAmC,CAAC,EAAE,CAAA;IACnE,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;IACzE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,GAAG,EAAE,EAAE;YACP,MAAM,EAAE,CAAC,sBAAsB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;SAChF,CAAA;IACH,CAAC;IAED,uDAAuD;IACvD,MAAM,MAAM,GAAQ,EAAE,CAAA;IACtB,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3C,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,wCAAwC,CAAC,EAAE,CAAA;IACxE,CAAC;IAED,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAClE,MAAM,EAAE,EAAE;KACX,CAAA;AACH,CAAC","sourcesContent":["/**\n * clearStyle — 특정 styling aspect 들을 초기화 (제거).\n *\n * preset 적용 전 / 데이터 바인딩 적용 전 깨끗한 상태로 돌리기 위한 helper.\n */\nimport type { SceneEditOp } from '@operato/scene-ops'\n\nexport interface ClearStyleInput {\n refids: number[]\n aspects: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity'>\n}\n\nexport interface OpsResult {\n ops: SceneEditOp[]\n errors: string[]\n}\n\nconst ASPECT_TO_PATCH: Record<string, any> = {\n fill: { fillStyle: { type: 'none' } },\n // stroke: strokeStyle 는 색상 문자열 (top-level), 굵기는 lineWidth (top-level).\n // 굵기 0 으로 윤곽선 시각 제거 (색은 유지). 완전 제거는 strokeStyle: null 동시 set.\n stroke: { lineWidth: 0 },\n shadow: { shadow: { blur: 0, color: 'transparent' } },\n material: { material3d: { preset: 'default' } },\n // font: null = delete 시맨틱 (apply-patch.ts:233-243). 핵심 폰트 키 모두 제거 →\n // component 가 기본 폰트 동작으로 복귀. 보존이 의도라면 font aspect 미선택.\n font: {\n fontSize: null,\n fontFamily: null,\n fontColor: null,\n fontWeight: null,\n fontStyle: null\n },\n opacity: { alpha: 1 }\n}\n\nexport function buildClearStyleOps(input: ClearStyleInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n if (!Array.isArray(input.aspects) || input.aspects.length === 0) {\n return { ops: [], errors: ['aspects must be a non-empty array'] }\n }\n const valid = ['fill', 'stroke', 'shadow', 'material', 'font', 'opacity']\n const unknown = input.aspects.filter(a => !valid.includes(a))\n if (unknown.length > 0) {\n return {\n ops: [],\n errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${valid.join('/')}`]\n }\n }\n\n // 모든 aspect 의 patch 를 하나로 병합 (한 refid 당 하나의 modify op)\n const merged: any = {}\n for (const a of input.aspects) {\n Object.assign(merged, ASPECT_TO_PATCH[a])\n }\n if (Object.keys(merged).length === 0) {\n return { ops: [], errors: ['No effective patch — try other aspects'] }\n }\n\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch: merged })),\n errors: []\n }\n}\n"]}
@@ -9,18 +9,18 @@
9
9
  * 입력 — sourceRefid 의 실제 styling 값을 미리 알 필요 없음. server 가 components
10
10
  * 에서 source 찾아 styling 추출 후 target 들에 patch 생성.
11
11
  */
12
- import type { BoardEditOp } from '../types';
12
+ import type { SceneEditOp } from '@operato/scene-ops';
13
13
  export interface CopyStyleInput {
14
14
  sourceRefid: number;
15
15
  targetRefids: number[];
16
16
  aspects?: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity' | 'visibility'>;
17
17
  }
18
18
  export interface OpsResult {
19
- ops: BoardEditOp[];
19
+ ops: SceneEditOp[];
20
20
  errors: string[];
21
21
  }
22
22
  /**
23
- * input + components 배열 → BoardEditOp[].
23
+ * input + components 배열 → SceneEditOp[].
24
24
  *
25
25
  * components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.
26
26
  */
@@ -29,7 +29,7 @@ const DEFAULT_ASPECTS = [
29
29
  // visibility 는 default 에서 제외 — 의도 없이 hidden 복사 위험
30
30
  ];
31
31
  /**
32
- * input + components 배열 → BoardEditOp[].
32
+ * input + components 배열 → SceneEditOp[].
33
33
  *
34
34
  * components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.
35
35
  */
@@ -1 +1 @@
1
- {"version":3,"file":"copy-tools.js","sourceRoot":"","sources":["../../../server/service/styling/copy-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * copyStyle — 한 컴포넌트의 styling 을 다른 컴포넌트들에 복사.\n *\n * 자주 쓰는 패턴: \"MOTOR1 처럼 다른 모터들도 같게 만들어줘\"\n * AI: getComponentByRefid(MOTOR1) 또는 getComponentStyle(MOTOR1) → copyStyle(...)\n *\n * aspects 인자로 어느 styling 영역만 복사할지 선택 가능 — 부분 통일 (예: 색만 통일).\n *\n * 입력 — sourceRefid 의 실제 styling 값을 미리 알 필요 없음. server 가 components\n * 에서 source 찾아 styling 추출 후 target 들에 patch 생성.\n */\nimport type { BoardEditOp } from '../types'\n\nexport interface CopyStyleInput {\n sourceRefid: number\n targetRefids: number[]\n aspects?: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity' | 'visibility'>\n}\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\nconst ASPECT_FIELDS: Record<string, string[]> = {\n fill: ['fillStyle'],\n stroke: ['strokeStyle'],\n shadow: ['shadow'],\n material: ['material3d'],\n font: [\n 'fontFamily',\n 'fontSize',\n 'fontWeight',\n 'fontStyle',\n 'fontColor',\n 'textAlign',\n 'textBaseline',\n 'lineHeight'\n ],\n opacity: ['alpha'],\n visibility: ['hidden']\n}\n\nconst DEFAULT_ASPECTS: Array<keyof typeof ASPECT_FIELDS> = [\n 'fill',\n 'stroke',\n 'shadow',\n 'material',\n 'font',\n 'opacity'\n // visibility 는 default 에서 제외 — 의도 없이 hidden 복사 위험\n]\n\n/**\n * input + components 배열 → BoardEditOp[].\n *\n * components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.\n */\nexport function buildCopyStyleOps(\n input: CopyStyleInput,\n components: ReadonlyArray<any>\n): OpsResult {\n if (typeof input.sourceRefid !== 'number') {\n return { ops: [], errors: ['sourceRefid (number) required'] }\n }\n if (!Array.isArray(input.targetRefids) || input.targetRefids.length === 0) {\n return { ops: [], errors: ['targetRefids must be a non-empty array'] }\n }\n const targets = input.targetRefids.filter(\n n => typeof n === 'number' && Number.isFinite(n) && n !== input.sourceRefid\n )\n if (targets.length === 0) {\n return { ops: [], errors: ['No valid target refids (excluding source)'] }\n }\n\n const source = components.find(\n c => c && typeof c.refid === 'number' && c.refid === input.sourceRefid\n )\n if (!source) {\n return {\n ops: [],\n errors: [`Source component refid=${input.sourceRefid} not found`]\n }\n }\n\n const aspects = input.aspects && input.aspects.length > 0 ? input.aspects : DEFAULT_ASPECTS\n const validAspects = Object.keys(ASPECT_FIELDS)\n const unknown = aspects.filter(a => !validAspects.includes(a))\n if (unknown.length > 0) {\n return {\n ops: [],\n errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${validAspects.join('/')}`]\n }\n }\n\n // source 에서 aspect 별 필드 추출 → patch 합치기\n const patch: any = {}\n for (const aspect of aspects) {\n const fields = ASPECT_FIELDS[aspect]\n for (const field of fields) {\n if (source[field] !== undefined) {\n patch[field] = source[field]\n }\n }\n }\n\n if (Object.keys(patch).length === 0) {\n return { ops: [], errors: ['Source has no styling for the requested aspects'] }\n }\n\n return {\n ops: targets.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n"]}
1
+ {"version":3,"file":"copy-tools.js","sourceRoot":"","sources":["../../../server/service/styling/copy-tools.ts"],"names":[],"mappings":";;AA0DA,8CAwDC;AA1FD,MAAM,aAAa,GAA6B;IAC9C,IAAI,EAAE,CAAC,WAAW,CAAC;IACnB,MAAM,EAAE,CAAC,aAAa,CAAC;IACvB,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,QAAQ,EAAE,CAAC,YAAY,CAAC;IACxB,IAAI,EAAE;QACJ,YAAY;QACZ,UAAU;QACV,YAAY;QACZ,WAAW;QACX,WAAW;QACX,WAAW;QACX,cAAc;QACd,YAAY;KACb;IACD,OAAO,EAAE,CAAC,OAAO,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,CAAC;CACvB,CAAA;AAED,MAAM,eAAe,GAAsC;IACzD,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,MAAM;IACN,SAAS;IACT,kDAAkD;CACnD,CAAA;AAED;;;;GAIG;AACH,SAAgB,iBAAiB,CAC/B,KAAqB,EACrB,UAA8B;IAE9B,IAAI,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;QAC1C,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,+BAA+B,CAAC,EAAE,CAAA;IAC/D,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1E,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,wCAAwC,CAAC,EAAE,CAAA;IACxE,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CACvC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,WAAW,CAC5E,CAAA;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,2CAA2C,CAAC,EAAE,CAAA;IAC3E,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAC5B,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,WAAW,CACvE,CAAA;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO;YACL,GAAG,EAAE,EAAE;YACP,MAAM,EAAE,CAAC,0BAA0B,KAAK,CAAC,WAAW,YAAY,CAAC;SAClE,CAAA;IACH,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAA;IAC3F,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,GAAG,EAAE,EAAE;YACP,MAAM,EAAE,CAAC,sBAAsB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;SACvF,CAAA;IACH,CAAC;IAED,uCAAuC;IACvC,MAAM,KAAK,GAAQ,EAAE,CAAA;IACrB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;QACpC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,SAAS,EAAE,CAAC;gBAChC,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,iDAAiD,CAAC,EAAE,CAAA;IACjF,CAAC;IAED,OAAO;QACL,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3D,MAAM,EAAE,EAAE;KACX,CAAA;AACH,CAAC","sourcesContent":["/**\n * copyStyle — 한 컴포넌트의 styling 을 다른 컴포넌트들에 복사.\n *\n * 자주 쓰는 패턴: \"MOTOR1 처럼 다른 모터들도 같게 만들어줘\"\n * AI: getComponentByRefid(MOTOR1) 또는 getComponentStyle(MOTOR1) → copyStyle(...)\n *\n * aspects 인자로 어느 styling 영역만 복사할지 선택 가능 — 부분 통일 (예: 색만 통일).\n *\n * 입력 — sourceRefid 의 실제 styling 값을 미리 알 필요 없음. server 가 components\n * 에서 source 찾아 styling 추출 후 target 들에 patch 생성.\n */\nimport type { SceneEditOp } from '@operato/scene-ops'\n\nexport interface CopyStyleInput {\n sourceRefid: number\n targetRefids: number[]\n aspects?: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity' | 'visibility'>\n}\n\nexport interface OpsResult {\n ops: SceneEditOp[]\n errors: string[]\n}\n\nconst ASPECT_FIELDS: Record<string, string[]> = {\n fill: ['fillStyle'],\n stroke: ['strokeStyle'],\n shadow: ['shadow'],\n material: ['material3d'],\n font: [\n 'fontFamily',\n 'fontSize',\n 'fontWeight',\n 'fontStyle',\n 'fontColor',\n 'textAlign',\n 'textBaseline',\n 'lineHeight'\n ],\n opacity: ['alpha'],\n visibility: ['hidden']\n}\n\nconst DEFAULT_ASPECTS: Array<keyof typeof ASPECT_FIELDS> = [\n 'fill',\n 'stroke',\n 'shadow',\n 'material',\n 'font',\n 'opacity'\n // visibility 는 default 에서 제외 — 의도 없이 hidden 복사 위험\n]\n\n/**\n * input + components 배열 → SceneEditOp[].\n *\n * components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.\n */\nexport function buildCopyStyleOps(\n input: CopyStyleInput,\n components: ReadonlyArray<any>\n): OpsResult {\n if (typeof input.sourceRefid !== 'number') {\n return { ops: [], errors: ['sourceRefid (number) required'] }\n }\n if (!Array.isArray(input.targetRefids) || input.targetRefids.length === 0) {\n return { ops: [], errors: ['targetRefids must be a non-empty array'] }\n }\n const targets = input.targetRefids.filter(\n n => typeof n === 'number' && Number.isFinite(n) && n !== input.sourceRefid\n )\n if (targets.length === 0) {\n return { ops: [], errors: ['No valid target refids (excluding source)'] }\n }\n\n const source = components.find(\n c => c && typeof c.refid === 'number' && c.refid === input.sourceRefid\n )\n if (!source) {\n return {\n ops: [],\n errors: [`Source component refid=${input.sourceRefid} not found`]\n }\n }\n\n const aspects = input.aspects && input.aspects.length > 0 ? input.aspects : DEFAULT_ASPECTS\n const validAspects = Object.keys(ASPECT_FIELDS)\n const unknown = aspects.filter(a => !validAspects.includes(a))\n if (unknown.length > 0) {\n return {\n ops: [],\n errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${validAspects.join('/')}`]\n }\n }\n\n // source 에서 aspect 별 필드 추출 → patch 합치기\n const patch: any = {}\n for (const aspect of aspects) {\n const fields = ASPECT_FIELDS[aspect]\n for (const field of fields) {\n if (source[field] !== undefined) {\n patch[field] = source[field]\n }\n }\n }\n\n if (Object.keys(patch).length === 0) {\n return { ops: [], errors: ['Source has no styling for the requested aspects'] }\n }\n\n return {\n ops: targets.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n"]}
@@ -5,7 +5,7 @@
5
5
  * setOpacity: state.alpha (컴포넌트 전체 알파, 0..1)
6
6
  * setVisible: state.hidden 토글 (true → 숨김)
7
7
  */
8
- import type { BoardEditOp } from '../types';
8
+ import type { SceneEditOp } from '@operato/scene-ops';
9
9
  export interface SetShadowInput {
10
10
  refids: number[];
11
11
  /** synthetic — false 면 blur=0 + color=transparent 매핑 (drawer 가 안 그림). 모델에 enabled 필드 없음. */
@@ -29,7 +29,7 @@ export interface SetVisibleInput {
29
29
  visible: boolean;
30
30
  }
31
31
  export interface OpsResult {
32
- ops: BoardEditOp[];
32
+ ops: SceneEditOp[];
33
33
  errors: string[];
34
34
  }
35
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"effect-tools.js","sourceRoot":"","sources":["../../../server/service/styling/effect-tools.ts"],"names":[],"mappings":";;AAgDA,8CA6CC;AAED,gDAaC;AAED,gDAaC;AAnHD,+CAA6D;AAgC7D;;;;;;;GAOG;AACH,SAAgB,iBAAiB,CAAC,KAAqB;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAA;IAClE,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,MAAM,IAAI,GACR,KAAK,CAAC,OAAO,KAAK,SAAS;QAC3B,KAAK,CAAC,KAAK,KAAK,SAAS;QACzB,KAAK,CAAC,IAAI,KAAK,SAAS;QACxB,KAAK,CAAC,OAAO,KAAK,SAAS;QAC3B,KAAK,CAAC,OAAO,KAAK,SAAS,CAAA;IAC7B,IAAI,IAAI;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,6BAA6B,CAAC,EAAE,CAAA;IAErE,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC5B,iEAAiE;QACjE,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAA;QAC3D,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;IACnF,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,IAAA,2BAAa,EAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,CAAC,KAAK;YAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,sBAAsB,CAAC,EAAE,CAAA;IAC/E,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACjF,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,iBAAiB,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;IAC7D,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACnE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,oBAAoB,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,CAAA;IACnE,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACnE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,oBAAoB,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,CAAA;IACnE,CAAC;IAED,2CAA2C;IAC3C,MAAM,EAAE,GAAoC,EAAE,CAAA;IAC9C,+DAA+D;IAC/D,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,EAAE,CAAC,KAAK,GAAG,IAAA,4BAAc,EAAC,KAAK,CAAC,KAAK,CAAE,CAAA;IACtE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IAClD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,EAAE,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAC3D,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,EAAE,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAE3D,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;IAC5B,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACnF,CAAC;AAED,SAAgB,kBAAkB,CAAC,KAAsB;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAA;IAClE,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1E,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,uBAAuB,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,CAAA;IACpE,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,mBAAmB;IACnB,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAA;IACpC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACnF,CAAC;AAED,SAAgB,kBAAkB,CAAC,KAAsB;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAA;IAClE,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,2BAA2B,CAAC,EAAE,CAAA;IAC3D,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,6BAA6B;IAC7B,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;IACxC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACnF,CAAC","sourcesContent":["/**\n * setShadow / setOpacity / setVisible — 효과 / 가시성 / 투명도.\n *\n * setShadow: state.shadow (color/blur/offsetX/offsetY)\n * setOpacity: state.alpha (컴포넌트 전체 알파, 0..1)\n * setVisible: state.hidden 토글 (true → 숨김)\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetShadowInput {\n refids: number[]\n /** synthetic — false 면 blur=0 + color=transparent 매핑 (drawer 가 안 그림). 모델에 enabled 필드 없음. */\n enabled?: boolean\n /** state.shadow.color */\n color?: string\n /** Blur radius (CSS box-shadow 의 blur-radius, Canvas 의 shadowBlur). state.shadow.blur. */\n blur?: number\n /** Horizontal offset (CSS box-shadow 의 offset-x, Canvas 의 shadowOffsetX). state.shadow.offsetX. */\n offsetX?: number\n /** Vertical offset (CSS box-shadow 의 offset-y, Canvas 의 shadowOffsetY). state.shadow.offsetY. */\n offsetY?: number\n}\n\nexport interface SetOpacityInput {\n refids: number[]\n /** state.alpha 의 canonical 이름. 0..1. */\n alpha: number\n}\n\nexport interface SetVisibleInput {\n refids: number[]\n visible: boolean\n}\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\n/**\n * setShadow — CSS box-shadow / Canvas API 표준 이름 (color/offsetX/offsetY/blur) 그대로\n * state.shadow 에 set. things-scene drawer 는 옛 이름 (left/top/blurSize) 도\n * backwards-compat 으로 읽지만 (effect/shadow.ts:36-47), 새 모델은 canonical 만 사용.\n *\n * synthetic enabled=false → blur 0 + transparent 매핑 (drawer 가 안 그림).\n * 모델에 enabled 필드는 없음.\n */\nexport function buildSetShadowOps(input: SetShadowInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n const noOp =\n input.enabled === undefined &&\n input.color === undefined &&\n input.blur === undefined &&\n input.offsetX === undefined &&\n input.offsetY === undefined\n if (noOp) return { ops: [], errors: ['No shadow changes specified'] }\n\n if (input.enabled === false) {\n // synthetic — false 면 blur 0 + transparent 으로 변환 (drawer 가 안 그림)\n const patch = { shadow: { blur: 0, color: 'transparent' } }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n }\n\n if (input.color !== undefined) {\n const v = validateColor(input.color)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid shadow color'] }\n }\n if (input.blur !== undefined && (input.blur < 0 || !Number.isFinite(input.blur))) {\n return { ops: [], errors: [`Invalid blur: ${input.blur}`] }\n }\n if (input.offsetX !== undefined && !Number.isFinite(input.offsetX)) {\n return { ops: [], errors: [`Invalid offsetX: ${input.offsetX}`] }\n }\n if (input.offsetY !== undefined && !Number.isFinite(input.offsetY)) {\n return { ops: [], errors: [`Invalid offsetY: ${input.offsetY}`] }\n }\n\n // CSS / Canvas 표준 canonical 이름 — rename 없음\n const sh: Record<string, number | string> = {}\n // validateColor 가 통과한 색만 도달 — normalizeColor 가 여기서 null 반환 안 함\n if (input.color !== undefined) sh.color = normalizeColor(input.color)!\n if (input.blur !== undefined) sh.blur = input.blur\n if (input.offsetX !== undefined) sh.offsetX = input.offsetX\n if (input.offsetY !== undefined) sh.offsetY = input.offsetY\n\n const patch = { shadow: sh }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n\nexport function buildSetOpacityOps(input: SetOpacityInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.alpha !== 'number' || input.alpha < 0 || input.alpha > 1) {\n return { ops: [], errors: [`alpha must be 0..1: ${input.alpha}`] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // canonical 이름 그대로\n const patch = { alpha: input.alpha }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n\nexport function buildSetVisibleOps(input: SetVisibleInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.visible !== 'boolean') {\n return { ops: [], errors: ['visible must be a boolean'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // hidden 은 visible 의 inverse\n const patch = { hidden: !input.visible }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n"]}
1
+ {"version":3,"file":"effect-tools.js","sourceRoot":"","sources":["../../../server/service/styling/effect-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setShadow / setOpacity / setVisible — 효과 / 가시성 / 투명도.\n *\n * setShadow: state.shadow (color/blur/offsetX/offsetY)\n * setOpacity: state.alpha (컴포넌트 전체 알파, 0..1)\n * setVisible: state.hidden 토글 (true → 숨김)\n */\nimport type { SceneEditOp } from '@operato/scene-ops'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetShadowInput {\n refids: number[]\n /** synthetic — false 면 blur=0 + color=transparent 매핑 (drawer 가 안 그림). 모델에 enabled 필드 없음. */\n enabled?: boolean\n /** state.shadow.color */\n color?: string\n /** Blur radius (CSS box-shadow 의 blur-radius, Canvas 의 shadowBlur). state.shadow.blur. */\n blur?: number\n /** Horizontal offset (CSS box-shadow 의 offset-x, Canvas 의 shadowOffsetX). state.shadow.offsetX. */\n offsetX?: number\n /** Vertical offset (CSS box-shadow 의 offset-y, Canvas 의 shadowOffsetY). state.shadow.offsetY. */\n offsetY?: number\n}\n\nexport interface SetOpacityInput {\n refids: number[]\n /** state.alpha 의 canonical 이름. 0..1. */\n alpha: number\n}\n\nexport interface SetVisibleInput {\n refids: number[]\n visible: boolean\n}\n\nexport interface OpsResult {\n ops: SceneEditOp[]\n errors: string[]\n}\n\n/**\n * setShadow — CSS box-shadow / Canvas API 표준 이름 (color/offsetX/offsetY/blur) 그대로\n * state.shadow 에 set. things-scene drawer 는 옛 이름 (left/top/blurSize) 도\n * backwards-compat 으로 읽지만 (effect/shadow.ts:36-47), 새 모델은 canonical 만 사용.\n *\n * synthetic enabled=false → blur 0 + transparent 매핑 (drawer 가 안 그림).\n * 모델에 enabled 필드는 없음.\n */\nexport function buildSetShadowOps(input: SetShadowInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n const noOp =\n input.enabled === undefined &&\n input.color === undefined &&\n input.blur === undefined &&\n input.offsetX === undefined &&\n input.offsetY === undefined\n if (noOp) return { ops: [], errors: ['No shadow changes specified'] }\n\n if (input.enabled === false) {\n // synthetic — false 면 blur 0 + transparent 으로 변환 (drawer 가 안 그림)\n const patch = { shadow: { blur: 0, color: 'transparent' } }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n }\n\n if (input.color !== undefined) {\n const v = validateColor(input.color)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid shadow color'] }\n }\n if (input.blur !== undefined && (input.blur < 0 || !Number.isFinite(input.blur))) {\n return { ops: [], errors: [`Invalid blur: ${input.blur}`] }\n }\n if (input.offsetX !== undefined && !Number.isFinite(input.offsetX)) {\n return { ops: [], errors: [`Invalid offsetX: ${input.offsetX}`] }\n }\n if (input.offsetY !== undefined && !Number.isFinite(input.offsetY)) {\n return { ops: [], errors: [`Invalid offsetY: ${input.offsetY}`] }\n }\n\n // CSS / Canvas 표준 canonical 이름 — rename 없음\n const sh: Record<string, number | string> = {}\n // validateColor 가 통과한 색만 도달 — normalizeColor 가 여기서 null 반환 안 함\n if (input.color !== undefined) sh.color = normalizeColor(input.color)!\n if (input.blur !== undefined) sh.blur = input.blur\n if (input.offsetX !== undefined) sh.offsetX = input.offsetX\n if (input.offsetY !== undefined) sh.offsetY = input.offsetY\n\n const patch = { shadow: sh }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n\nexport function buildSetOpacityOps(input: SetOpacityInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.alpha !== 'number' || input.alpha < 0 || input.alpha > 1) {\n return { ops: [], errors: [`alpha must be 0..1: ${input.alpha}`] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // canonical 이름 그대로\n const patch = { alpha: input.alpha }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n\nexport function buildSetVisibleOps(input: SetVisibleInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.visible !== 'boolean') {\n return { ops: [], errors: ['visible must be a boolean'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // hidden 은 visible 의 inverse\n const patch = { hidden: !input.visible }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n"]}
@@ -17,7 +17,7 @@
17
17
  * 이전 2-op (null + new) 방식은 중간 null 상태가 onchange / 3D texture
18
18
  * 크래시 유발 → 제거. 정확한 colorStops 출력으로 stale 필드 문제는 자체 해결.
19
19
  */
20
- import type { BoardEditOp } from '../types';
20
+ import type { SceneEditOp } from '@operato/scene-ops';
21
21
  export interface SetFillInput {
22
22
  refids: number[];
23
23
  mode?: 'none' | 'solid' | 'gradient' | 'pattern';
@@ -46,11 +46,11 @@ export interface SetFillInput {
46
46
  opacity?: number;
47
47
  }
48
48
  export interface SetFillResult {
49
- ops: BoardEditOp[];
49
+ ops: SceneEditOp[];
50
50
  errors: string[];
51
51
  }
52
52
  /**
53
- * setFill 입력 검증 + BoardEditOp[] 생성.
53
+ * setFill 입력 검증 + SceneEditOp[] 생성.
54
54
  *
55
55
  * 기본 정책:
56
56
  * - refids 비어있거나 mode 도 색도 없으면 에러
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildSetFillOps = buildSetFillOps;
4
4
  const color_utils_1 = require("./color-utils");
5
5
  /**
6
- * setFill 입력 검증 + BoardEditOp[] 생성.
6
+ * setFill 입력 검증 + SceneEditOp[] 생성.
7
7
  *
8
8
  * 기본 정책:
9
9
  * - refids 비어있거나 mode 도 색도 없으면 에러