@things-factory/board-import 10.0.0-beta.71 → 10.0.0-beta.73

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 (83) hide show
  1. package/dist-server/index.d.ts +1 -1
  2. package/dist-server/index.js +1 -1
  3. package/dist-server/index.js.map +1 -1
  4. package/dist-server/service/adapters/dxf-adapter.d.ts +1 -1
  5. package/dist-server/service/adapters/dxf-adapter.js +18 -4
  6. package/dist-server/service/adapters/dxf-adapter.js.map +1 -1
  7. package/dist-server/service/adapters/image-adapter.d.ts +32 -0
  8. package/dist-server/service/adapters/image-adapter.js +420 -0
  9. package/dist-server/service/adapters/image-adapter.js.map +1 -0
  10. package/dist-server/service/ai/types.d.ts +96 -4
  11. package/dist-server/service/ai/types.js +221 -12
  12. package/dist-server/service/ai/types.js.map +1 -1
  13. package/dist-server/service/converters/generic-to.d.ts +17 -8
  14. package/dist-server/service/converters/generic-to.js +44 -44
  15. package/dist-server/service/converters/generic-to.js.map +1 -1
  16. package/dist-server/service/import-session/import-actions.d.ts +79 -0
  17. package/dist-server/service/import-session/import-actions.js +98 -0
  18. package/dist-server/service/import-session/import-actions.js.map +1 -0
  19. package/dist-server/service/import-session/import-session-resolver.d.ts +22 -1
  20. package/dist-server/service/import-session/import-session-resolver.js +166 -38
  21. package/dist-server/service/import-session/import-session-resolver.js.map +1 -1
  22. package/dist-server/service/import-session/import-worker.js +55 -13
  23. package/dist-server/service/import-session/import-worker.js.map +1 -1
  24. package/dist-server/service/import-session/index.d.ts +5 -3
  25. package/dist-server/service/import-session/index.js +16 -9
  26. package/dist-server/service/import-session/index.js.map +1 -1
  27. package/dist-server/service/import-session/materialize-from-session.d.ts +66 -0
  28. package/dist-server/service/import-session/materialize-from-session.js +104 -0
  29. package/dist-server/service/import-session/materialize-from-session.js.map +1 -0
  30. package/dist-server/service/import-session/suggest-board-name.d.ts +80 -0
  31. package/dist-server/service/import-session/suggest-board-name.js +195 -0
  32. package/dist-server/service/import-session/suggest-board-name.js.map +1 -0
  33. package/dist-server/service/import-tools.d.ts +10 -0
  34. package/dist-server/service/import-tools.js +227 -0
  35. package/dist-server/service/import-tools.js.map +1 -0
  36. package/dist-server/service/index.d.ts +22 -19
  37. package/dist-server/service/index.js +46 -36
  38. package/dist-server/service/index.js.map +1 -1
  39. package/dist-server/service/pipeline/index.d.ts +42 -9
  40. package/dist-server/service/pipeline/index.js +92 -23
  41. package/dist-server/service/pipeline/index.js.map +1 -1
  42. package/dist-server/service/pipeline/stage2-mapping.d.ts +20 -5
  43. package/dist-server/service/pipeline/stage2-mapping.js +84 -26
  44. package/dist-server/service/pipeline/stage2-mapping.js.map +1 -1
  45. package/dist-server/service/pipeline/stage3-board.d.ts +2 -2
  46. package/dist-server/service/pipeline/stage3-board.js +65 -10
  47. package/dist-server/service/pipeline/stage3-board.js.map +1 -1
  48. package/dist-server/service/pipeline/stage4-binding.d.ts +1 -1
  49. package/dist-server/service/pipeline/stage4-binding.js +2 -2
  50. package/dist-server/service/pipeline/stage4-binding.js.map +1 -1
  51. package/dist-server/service/registry/index.d.ts +1 -1
  52. package/dist-server/service/registry/index.js.map +1 -1
  53. package/dist-server/service/types/index.d.ts +35 -0
  54. package/dist-server/service/types/index.js.map +1 -1
  55. package/dist-server/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +7 -6
  57. package/server/index.ts +1 -1
  58. package/server/service/adapters/dxf-adapter.ts +17 -4
  59. package/server/service/adapters/image-adapter.test.ts +545 -0
  60. package/server/service/adapters/image-adapter.ts +464 -0
  61. package/server/service/ai/types.ts +354 -19
  62. package/server/service/converters/generic-to.test.ts +91 -0
  63. package/server/service/converters/generic-to.ts +66 -49
  64. package/server/service/import-session/import-actions.test.ts +185 -0
  65. package/server/service/import-session/import-actions.ts +164 -0
  66. package/server/service/import-session/import-session-resolver.ts +171 -39
  67. package/server/service/import-session/import-worker.ts +56 -12
  68. package/server/service/import-session/index.ts +19 -3
  69. package/server/service/import-session/materialize-from-session.test.ts +274 -0
  70. package/server/service/import-session/materialize-from-session.ts +158 -0
  71. package/server/service/import-session/suggest-board-name.test.ts +271 -0
  72. package/server/service/import-session/suggest-board-name.ts +279 -0
  73. package/server/service/import-tools.test.ts +137 -0
  74. package/server/service/import-tools.ts +255 -0
  75. package/server/service/index.ts +35 -18
  76. package/server/service/pipeline/index.ts +118 -23
  77. package/server/service/pipeline/stage2-mapping.test.ts +204 -0
  78. package/server/service/pipeline/stage2-mapping.ts +102 -27
  79. package/server/service/pipeline/stage3-board.test.ts +133 -0
  80. package/server/service/pipeline/stage3-board.ts +76 -11
  81. package/server/service/pipeline/stage4-binding.ts +2 -2
  82. package/server/service/registry/index.ts +1 -1
  83. package/server/service/types/index.ts +36 -0
@@ -15,10 +15,86 @@ const tslib_1 = require("tslib");
15
15
  */
16
16
  const type_graphql_1 = require("type-graphql");
17
17
  const graphql_scalars_1 = require("graphql-scalars");
18
+ const typeorm_1 = require("typeorm");
18
19
  require("@things-factory/auth-base");
19
20
  const shell_1 = require("@things-factory/shell");
20
- const import_session_js_1 = require("./import-session.js");
21
- const import_worker_js_1 = require("./import-worker.js");
21
+ const board_service_1 = require("@things-factory/board-service");
22
+ const attachment_base_1 = require("@things-factory/attachment-base");
23
+ const import_session_1 = require("./import-session");
24
+ const materialize_from_session_1 = require("./materialize-from-session");
25
+ const suggest_board_name_1 = require("./suggest-board-name");
26
+ const import_actions_1 = require("./import-actions");
27
+ let MaterializeImportSessionInput = class MaterializeImportSessionInput {
28
+ };
29
+ tslib_1.__decorate([
30
+ (0, type_graphql_1.Field)(() => type_graphql_1.ID, { description: 'ImportSession id (must be in completed state).' }),
31
+ tslib_1.__metadata("design:type", String)
32
+ ], MaterializeImportSessionInput.prototype, "sessionId", void 0);
33
+ tslib_1.__decorate([
34
+ (0, type_graphql_1.Field)({ description: 'New Board name.' }),
35
+ tslib_1.__metadata("design:type", String)
36
+ ], MaterializeImportSessionInput.prototype, "name", void 0);
37
+ tslib_1.__decorate([
38
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Optional Board description.' }),
39
+ tslib_1.__metadata("design:type", String)
40
+ ], MaterializeImportSessionInput.prototype, "description", void 0);
41
+ tslib_1.__decorate([
42
+ (0, type_graphql_1.Field)(() => type_graphql_1.ID, { nullable: true, description: 'Optional Group id to attach the Board to.' }),
43
+ tslib_1.__metadata("design:type", String)
44
+ ], MaterializeImportSessionInput.prototype, "groupId", void 0);
45
+ tslib_1.__decorate([
46
+ (0, type_graphql_1.Field)({ nullable: true, description: "Board type — 'main' | 'sub' | 'popup'. Default 'main'." }),
47
+ tslib_1.__metadata("design:type", String)
48
+ ], MaterializeImportSessionInput.prototype, "type", void 0);
49
+ tslib_1.__decorate([
50
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Base64 thumbnail. If omitted, board-service default empty thumbnail is used.' }),
51
+ tslib_1.__metadata("design:type", String)
52
+ ], MaterializeImportSessionInput.prototype, "thumbnail", void 0);
53
+ tslib_1.__decorate([
54
+ (0, type_graphql_1.Field)({
55
+ nullable: true,
56
+ description: "ImportSession 의 결과 시안 중 하나의 id. 'as-is' / 'scene' / 'auto-fit'. " +
57
+ '미지정 시 default variant (보통 \'scene\') 사용.'
58
+ }),
59
+ tslib_1.__metadata("design:type", String)
60
+ ], MaterializeImportSessionInput.prototype, "variantId", void 0);
61
+ MaterializeImportSessionInput = tslib_1.__decorate([
62
+ (0, type_graphql_1.InputType)()
63
+ ], MaterializeImportSessionInput);
64
+ let BoardMetaSuggestion = class BoardMetaSuggestion {
65
+ };
66
+ tslib_1.__decorate([
67
+ (0, type_graphql_1.Field)({ description: 'Suggested short board name (collision-free with current domain).' }),
68
+ tslib_1.__metadata("design:type", String)
69
+ ], BoardMetaSuggestion.prototype, "name", void 0);
70
+ tslib_1.__decorate([
71
+ (0, type_graphql_1.Field)({
72
+ description: 'Suggested detailed description — combines user prompt, AI importStrategy narrative, and category distribution stats.'
73
+ }),
74
+ tslib_1.__metadata("design:type", String)
75
+ ], BoardMetaSuggestion.prototype, "description", void 0);
76
+ BoardMetaSuggestion = tslib_1.__decorate([
77
+ (0, type_graphql_1.ObjectType)()
78
+ ], BoardMetaSuggestion);
79
+ let SuggestBoardNameInput = class SuggestBoardNameInput {
80
+ };
81
+ tslib_1.__decorate([
82
+ (0, type_graphql_1.Field)(() => type_graphql_1.ID, {
83
+ nullable: true,
84
+ description: 'ImportSession id — 있으면 거기서 userPrompt / VLM reasoning / attachment.name 추출.'
85
+ }),
86
+ tslib_1.__metadata("design:type", String)
87
+ ], SuggestBoardNameInput.prototype, "sessionId", void 0);
88
+ tslib_1.__decorate([
89
+ (0, type_graphql_1.Field)({
90
+ nullable: true,
91
+ description: '명시 hint. session 정보보다 우선. 사용자가 직접 적은 메모를 기반으로 추천받고 싶을 때.'
92
+ }),
93
+ tslib_1.__metadata("design:type", String)
94
+ ], SuggestBoardNameInput.prototype, "hint", void 0);
95
+ SuggestBoardNameInput = tslib_1.__decorate([
96
+ (0, type_graphql_1.InputType)()
97
+ ], SuggestBoardNameInput);
22
98
  let ImportBoardAsyncInput = class ImportBoardAsyncInput {
23
99
  };
24
100
  tslib_1.__decorate([
@@ -53,55 +129,70 @@ tslib_1.__decorate([
53
129
  (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, { nullable: true, description: 'Adapter parse options (excludeLayers, maxEntities, ...).' }),
54
130
  tslib_1.__metadata("design:type", Object)
55
131
  ], ImportBoardAsyncInput.prototype, "parseOptions", void 0);
132
+ tslib_1.__decorate([
133
+ (0, type_graphql_1.Field)({
134
+ nullable: true,
135
+ description: 'User-provided context / hints about the drawing — passed to VLM as additional guidance. ' +
136
+ 'e.g. "fab lithography zone, central rectangles are stockers, thin lines are OHT rails."'
137
+ }),
138
+ tslib_1.__metadata("design:type", String)
139
+ ], ImportBoardAsyncInput.prototype, "userPrompt", void 0);
56
140
  ImportBoardAsyncInput = tslib_1.__decorate([
57
141
  (0, type_graphql_1.InputType)()
58
142
  ], ImportBoardAsyncInput);
59
143
  let ImportSessionResolver = class ImportSessionResolver {
60
144
  async importSession(id, context) {
61
145
  const { domain } = context.state;
62
- return (await (0, shell_1.getRepository)(import_session_js_1.ImportSession).findOneBy({ id, domain: { id: domain.id } })) ?? null;
146
+ return await (0, import_actions_1.fetchImportSession)(id, { domain, user: context.state.user }, (0, shell_1.getRepository)(import_session_1.ImportSession));
63
147
  }
64
148
  async importSessionsByChatSession(chatSessionId, context) {
65
- const { domain } = context.state;
66
- return await (0, shell_1.getRepository)(import_session_js_1.ImportSession).find({
67
- where: { chatSessionId, domain: { id: domain.id } },
68
- order: { createdAt: 'DESC' },
69
- take: 50
70
- });
149
+ const { domain, user } = context.state;
150
+ return await (0, import_actions_1.listImportSessionsByChatSession)(chatSessionId, { domain, user }, (0, shell_1.getRepository)(import_session_1.ImportSession));
71
151
  }
72
152
  async importBoardAsync(input, context) {
73
153
  const { domain, user } = context.state;
74
- const repo = (0, shell_1.getRepository)(import_session_js_1.ImportSession);
75
- const session = await repo.save({
76
- domain,
77
- attachmentId: input.attachmentId,
78
- chatSessionId: input.chatSessionId,
79
- status: 'queued',
80
- progress: 0,
81
- message: 'Queued',
82
- optionsJson: JSON.stringify({
83
- scopes: input.scopes,
84
- normalizeOrigin: input.normalizeOrigin,
85
- flipY: input.flipY,
86
- scale: input.scale,
87
- applyBinding: input.applyBinding,
88
- parseOptions: input.parseOptions
89
- }),
90
- creator: user
91
- });
92
- // 비동기 worker 시작 — 응답은 이미 즉시 반환됨
93
- setImmediate(() => {
94
- (0, import_worker_js_1.runImportJob)(session.id).catch(err => {
95
- // worker 내부에서 처리되지만 안전망
96
- console.error('[board-import] runImportJob unhandled:', err);
97
- });
98
- });
99
- return session;
154
+ return await (0, import_actions_1.startImportJobAsync)(input, { domain, user }, (0, shell_1.getRepository)(import_session_1.ImportSession));
155
+ }
156
+ async suggestBoardName(input, context) {
157
+ const { domain } = context.state;
158
+ const deps = this._buildSuggestDeps();
159
+ return await (0, suggest_board_name_1.suggestBoardName)(input, { domain }, deps);
160
+ }
161
+ async suggestBoardMeta(input, context) {
162
+ const { domain } = context.state;
163
+ const deps = this._buildSuggestDeps();
164
+ return await (0, suggest_board_name_1.suggestBoardMeta)(input, { domain }, deps);
165
+ }
166
+ /** 두 query 가 공유하는 deps wiring — typeorm find idiomatic 패턴 (distinctAlias 회귀 회피). */
167
+ _buildSuggestDeps() {
168
+ const sessionRepo = (0, shell_1.getRepository)(import_session_1.ImportSession);
169
+ const attachmentRepo = (0, shell_1.getRepository)(attachment_base_1.Attachment);
170
+ const boardRepo = {
171
+ findExistingNames: async ({ domainId, basePrefix }) => {
172
+ // 도메인 격리 + 'basePrefix' 로 시작하는 name 후보 한 번에 조회.
173
+ // 단순 find({ where, select }) — take 없음 → DISTINCT 서브쿼리 미발생.
174
+ const escaped = basePrefix.replace(/[%_\\]/g, ch => '\\' + ch);
175
+ const items = await (0, shell_1.getRepository)(board_service_1.Board).find({
176
+ select: ['name'],
177
+ where: { domain: { id: domainId }, name: (0, typeorm_1.Like)(`${escaped}%`) }
178
+ });
179
+ return items.map(b => b.name);
180
+ }
181
+ };
182
+ return { sessionRepo, attachmentRepo, boardRepo };
183
+ }
184
+ async materializeImportSession(input, context) {
185
+ const { domain, user } = context.state;
186
+ const sessionRepo = (0, shell_1.getRepository)(import_session_1.ImportSession);
187
+ const boardRepo = (0, shell_1.getRepository)(board_service_1.Board);
188
+ const groupRepo = (0, shell_1.getRepository)(board_service_1.Group);
189
+ const session = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } });
190
+ return (await (0, materialize_from_session_1.materializeFromSession)(session, input, { domain, user }, { boardRepo: boardRepo, groupRepo: groupRepo }));
100
191
  }
101
192
  };
102
193
  exports.ImportSessionResolver = ImportSessionResolver;
103
194
  tslib_1.__decorate([
104
- (0, type_graphql_1.Query)(() => import_session_js_1.ImportSession, { nullable: true, description: 'Get import session progress.' }),
195
+ (0, type_graphql_1.Query)(() => import_session_1.ImportSession, { nullable: true, description: 'Get import session progress.' }),
105
196
  (0, type_graphql_1.Directive)('@privilege(category: "board-import", privilege: "query")'),
106
197
  tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
107
198
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
@@ -110,7 +201,7 @@ tslib_1.__decorate([
110
201
  tslib_1.__metadata("design:returntype", Promise)
111
202
  ], ImportSessionResolver.prototype, "importSession", null);
112
203
  tslib_1.__decorate([
113
- (0, type_graphql_1.Query)(() => [import_session_js_1.ImportSession], { description: 'List recent import sessions for a chat session.' }),
204
+ (0, type_graphql_1.Query)(() => [import_session_1.ImportSession], { description: 'List recent import sessions for a chat session.' }),
114
205
  (0, type_graphql_1.Directive)('@privilege(category: "board-import", privilege: "query")'),
115
206
  tslib_1.__param(0, (0, type_graphql_1.Arg)('chatSessionId')),
116
207
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
@@ -119,7 +210,7 @@ tslib_1.__decorate([
119
210
  tslib_1.__metadata("design:returntype", Promise)
120
211
  ], ImportSessionResolver.prototype, "importSessionsByChatSession", null);
121
212
  tslib_1.__decorate([
122
- (0, type_graphql_1.Mutation)(() => import_session_js_1.ImportSession, {
213
+ (0, type_graphql_1.Mutation)(() => import_session_1.ImportSession, {
123
214
  description: '도면 → 보드 변환을 비동기로 시작. 즉시 ImportSession 반환, 백그라운드에서 처리.'
124
215
  }),
125
216
  (0, type_graphql_1.Directive)('@privilege(category: "board-import", privilege: "mutation")'),
@@ -129,6 +220,43 @@ tslib_1.__decorate([
129
220
  tslib_1.__metadata("design:paramtypes", [ImportBoardAsyncInput, Object]),
130
221
  tslib_1.__metadata("design:returntype", Promise)
131
222
  ], ImportSessionResolver.prototype, "importBoardAsync", null);
223
+ tslib_1.__decorate([
224
+ (0, type_graphql_1.Query)(() => String, {
225
+ description: '@deprecated suggestBoardMeta 사용 권장. 단순 string name 만 반환하는 구버전 query.'
226
+ }),
227
+ (0, type_graphql_1.Directive)('@privilege(category: "board-import", privilege: "query")'),
228
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('input')),
229
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
230
+ tslib_1.__metadata("design:type", Function),
231
+ tslib_1.__metadata("design:paramtypes", [SuggestBoardNameInput, Object]),
232
+ tslib_1.__metadata("design:returntype", Promise)
233
+ ], ImportSessionResolver.prototype, "suggestBoardName", null);
234
+ tslib_1.__decorate([
235
+ (0, type_graphql_1.Query)(() => BoardMetaSuggestion, {
236
+ description: '도메인 안에서 충돌하지 않는 Board name + 상세 description 제안. ' +
237
+ 'name 은 짧고 심플 (30자 이내, 충돌 회피 (n) suffix), description 은 자세히 ' +
238
+ '(사용자 prompt + AI importStrategy + 카테고리 분포 통계 합성).'
239
+ }),
240
+ (0, type_graphql_1.Directive)('@privilege(category: "board-import", privilege: "query")'),
241
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('input')),
242
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
243
+ tslib_1.__metadata("design:type", Function),
244
+ tslib_1.__metadata("design:paramtypes", [SuggestBoardNameInput, Object]),
245
+ tslib_1.__metadata("design:returntype", Promise)
246
+ ], ImportSessionResolver.prototype, "suggestBoardMeta", null);
247
+ tslib_1.__decorate([
248
+ (0, type_graphql_1.Mutation)(() => board_service_1.Board, {
249
+ description: '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화한다. ' +
250
+ '검수 단계 (사용자/AI 가 import 결과를 확인 후 명시적으로 호출) 에서 사용. ' +
251
+ 'Board.state="draft" 로 생성되므로 release mutation 으로 별도 발행 필요.'
252
+ }),
253
+ (0, type_graphql_1.Directive)('@privilege(category: "board-import", privilege: "mutation")'),
254
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('input')),
255
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
256
+ tslib_1.__metadata("design:type", Function),
257
+ tslib_1.__metadata("design:paramtypes", [MaterializeImportSessionInput, Object]),
258
+ tslib_1.__metadata("design:returntype", Promise)
259
+ ], ImportSessionResolver.prototype, "materializeImportSession", null);
132
260
  exports.ImportSessionResolver = ImportSessionResolver = tslib_1.__decorate([
133
261
  (0, type_graphql_1.Resolver)()
134
262
  ], ImportSessionResolver);
@@ -1 +1 @@
1
- {"version":3,"file":"import-session-resolver.js","sourceRoot":"","sources":["../../../server/service/import-session/import-session-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ImportSession resolver + 비동기 worker.\n *\n * Mutation:\n * - importBoardAsync(attachmentId, options): ImportSession (즉시 반환, 백그라운드에서 처리)\n *\n * Query:\n * - importSession(id): ImportSession (진행 상황 polling)\n *\n * 비동기 worker 는 단일 프로세스 내 setImmediate 기반. 추후 큐(Redis/BullMQ) 로 이전 가능.\n */\nimport { Resolver, Query, Mutation, Arg, Ctx, Directive, Field, InputType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport '@things-factory/auth-base'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { ImportSession } from './import-session.js'\nimport { runImportJob } from './import-worker.js'\n\n@InputType()\nclass ImportBoardAsyncInput {\n @Field({ description: 'Attachment id of the source drawing file (uploaded via attachment-base).' })\n attachmentId!: string\n\n @Field({ nullable: true, description: 'ChatSession id when triggered from board-ai chat.' })\n chatSessionId?: string\n\n @Field(() => [String], { nullable: true, description: 'Registry scopes to use (board-import).' })\n scopes?: string[]\n\n @Field({ nullable: true, description: 'Normalize coordinates so minX,minY=0' })\n normalizeOrigin?: boolean\n\n @Field({ nullable: true, description: 'Flip Y axis (CAD Y-up → scene Y-down).' })\n flipY?: boolean\n\n @Field({ nullable: true, description: 'Scale factor (e.g. 1 for mm:1unit).' })\n scale?: number\n\n @Field({ nullable: true, description: 'Apply Stage 4 (data binding).' })\n applyBinding?: boolean\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Adapter parse options (excludeLayers, maxEntities, ...).' })\n parseOptions?: any\n}\n\n@Resolver()\nexport class ImportSessionResolver {\n @Query(() => ImportSession, { nullable: true, description: 'Get import session progress.' })\n @Directive('@privilege(category: \"board-import\", privilege: \"query\")')\n async importSession(\n @Arg('id') id: string,\n @Ctx() context: ResolverContext\n ): Promise<ImportSession | null> {\n const { domain } = context.state\n return (await getRepository(ImportSession).findOneBy({ id, domain: { id: domain.id } })) ?? null\n }\n\n @Query(() => [ImportSession], { description: 'List recent import sessions for a chat session.' })\n @Directive('@privilege(category: \"board-import\", privilege: \"query\")')\n async importSessionsByChatSession(\n @Arg('chatSessionId') chatSessionId: string,\n @Ctx() context: ResolverContext\n ): Promise<ImportSession[]> {\n const { domain } = context.state\n return await getRepository(ImportSession).find({\n where: { chatSessionId, domain: { id: domain.id } },\n order: { createdAt: 'DESC' },\n take: 50\n })\n }\n\n @Mutation(() => ImportSession, {\n description: '도면 → 보드 변환을 비동기로 시작. 즉시 ImportSession 반환, 백그라운드에서 처리.'\n })\n @Directive('@privilege(category: \"board-import\", privilege: \"mutation\")')\n async importBoardAsync(\n @Arg('input') input: ImportBoardAsyncInput,\n @Ctx() context: ResolverContext\n ): Promise<ImportSession> {\n const { domain, user } = context.state\n const repo = getRepository(ImportSession)\n\n const session = await repo.save({\n domain,\n attachmentId: input.attachmentId,\n chatSessionId: input.chatSessionId,\n status: 'queued',\n progress: 0,\n message: 'Queued',\n optionsJson: JSON.stringify({\n scopes: input.scopes,\n normalizeOrigin: input.normalizeOrigin,\n flipY: input.flipY,\n scale: input.scale,\n applyBinding: input.applyBinding,\n parseOptions: input.parseOptions\n }),\n creator: user\n } as any)\n\n // 비동기 worker 시작 — 응답은 이미 즉시 반환됨\n setImmediate(() => {\n runImportJob(session.id!).catch(err => {\n // worker 내부에서 처리되지만 안전망\n console.error('[board-import] runImportJob unhandled:', err)\n })\n })\n\n return session\n }\n}\n"]}
1
+ {"version":3,"file":"import-session-resolver.js","sourceRoot":"","sources":["../../../server/service/import-session/import-session-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ImportSession resolver + 비동기 worker.\n *\n * Mutation:\n * - importBoardAsync(attachmentId, options): ImportSession (즉시 반환, 백그라운드에서 처리)\n *\n * Query:\n * - importSession(id): ImportSession (진행 상황 polling)\n *\n * 비동기 worker 는 단일 프로세스 내 setImmediate 기반. 추후 큐(Redis/BullMQ) 로 이전 가능.\n */\nimport {\n Resolver,\n Query,\n Mutation,\n Arg,\n Ctx,\n Directive,\n Field,\n ID,\n InputType,\n ObjectType\n} from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport { Like } from 'typeorm'\nimport '@things-factory/auth-base'\n\nimport { getRepository } from '@things-factory/shell'\nimport { Board, Group } from '@things-factory/board-service'\nimport { Attachment } from '@things-factory/attachment-base'\n\nimport { ImportSession } from './import-session'\nimport { materializeFromSession } from './materialize-from-session'\nimport {\n suggestBoardName,\n suggestBoardMeta,\n type SuggestBoardMetaResult,\n type SuggestBoardRepoLike\n} from './suggest-board-name'\nimport {\n startImportJobAsync,\n fetchImportSession,\n listImportSessionsByChatSession\n} from './import-actions'\n\n@InputType()\nclass MaterializeImportSessionInput {\n @Field(() => ID, { description: 'ImportSession id (must be in completed state).' })\n sessionId!: string\n\n @Field({ description: 'New Board name.' })\n name!: string\n\n @Field({ nullable: true, description: 'Optional Board description.' })\n description?: string\n\n @Field(() => ID, { nullable: true, description: 'Optional Group id to attach the Board to.' })\n groupId?: string\n\n @Field({ nullable: true, description: \"Board type — 'main' | 'sub' | 'popup'. Default 'main'.\" })\n type?: 'main' | 'sub' | 'popup'\n\n @Field({ nullable: true, description: 'Base64 thumbnail. If omitted, board-service default empty thumbnail is used.' })\n thumbnail?: string\n\n @Field({\n nullable: true,\n description:\n \"ImportSession 의 결과 시안 중 하나의 id. 'as-is' / 'scene' / 'auto-fit'. \" +\n '미지정 시 default variant (보통 \\'scene\\') 사용.'\n })\n variantId?: string\n}\n\n@ObjectType()\nclass BoardMetaSuggestion {\n @Field({ description: 'Suggested short board name (collision-free with current domain).' })\n name!: string\n\n @Field({\n description:\n 'Suggested detailed description — combines user prompt, AI importStrategy narrative, and category distribution stats.'\n })\n description!: string\n}\n\n@InputType()\nclass SuggestBoardNameInput {\n @Field(() => ID, {\n nullable: true,\n description: 'ImportSession id — 있으면 거기서 userPrompt / VLM reasoning / attachment.name 추출.'\n })\n sessionId?: string\n\n @Field({\n nullable: true,\n description: '명시 hint. session 정보보다 우선. 사용자가 직접 적은 메모를 기반으로 추천받고 싶을 때.'\n })\n hint?: string\n}\n\n@InputType()\nclass ImportBoardAsyncInput {\n @Field({ description: 'Attachment id of the source drawing file (uploaded via attachment-base).' })\n attachmentId!: string\n\n @Field({ nullable: true, description: 'ChatSession id when triggered from board-ai chat.' })\n chatSessionId?: string\n\n @Field(() => [String], { nullable: true, description: 'Registry scopes to use (board-import).' })\n scopes?: string[]\n\n @Field({ nullable: true, description: 'Normalize coordinates so minX,minY=0' })\n normalizeOrigin?: boolean\n\n @Field({ nullable: true, description: 'Flip Y axis (CAD Y-up → scene Y-down).' })\n flipY?: boolean\n\n @Field({ nullable: true, description: 'Scale factor (e.g. 1 for mm:1unit).' })\n scale?: number\n\n @Field({ nullable: true, description: 'Apply Stage 4 (data binding).' })\n applyBinding?: boolean\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Adapter parse options (excludeLayers, maxEntities, ...).' })\n parseOptions?: any\n\n @Field({\n nullable: true,\n description:\n 'User-provided context / hints about the drawing — passed to VLM as additional guidance. ' +\n 'e.g. \"fab lithography zone, central rectangles are stockers, thin lines are OHT rails.\"'\n })\n userPrompt?: string\n}\n\n@Resolver()\nexport class ImportSessionResolver {\n @Query(() => ImportSession, { nullable: true, description: 'Get import session progress.' })\n @Directive('@privilege(category: \"board-import\", privilege: \"query\")')\n async importSession(\n @Arg('id') id: string,\n @Ctx() context: ResolverContext\n ): Promise<ImportSession | null> {\n const { domain } = context.state\n return await fetchImportSession(id, { domain, user: context.state.user }, getRepository(ImportSession) as any)\n }\n\n @Query(() => [ImportSession], { description: 'List recent import sessions for a chat session.' })\n @Directive('@privilege(category: \"board-import\", privilege: \"query\")')\n async importSessionsByChatSession(\n @Arg('chatSessionId') chatSessionId: string,\n @Ctx() context: ResolverContext\n ): Promise<ImportSession[]> {\n const { domain, user } = context.state\n return await listImportSessionsByChatSession(chatSessionId, { domain, user }, getRepository(ImportSession) as any)\n }\n\n @Mutation(() => ImportSession, {\n description: '도면 → 보드 변환을 비동기로 시작. 즉시 ImportSession 반환, 백그라운드에서 처리.'\n })\n @Directive('@privilege(category: \"board-import\", privilege: \"mutation\")')\n async importBoardAsync(\n @Arg('input') input: ImportBoardAsyncInput,\n @Ctx() context: ResolverContext\n ): Promise<ImportSession> {\n const { domain, user } = context.state\n return await startImportJobAsync(input, { domain, user }, getRepository(ImportSession) as any)\n }\n\n @Query(() => String, {\n description:\n '@deprecated suggestBoardMeta 사용 권장. 단순 string name 만 반환하는 구버전 query.'\n })\n @Directive('@privilege(category: \"board-import\", privilege: \"query\")')\n async suggestBoardName(\n @Arg('input') input: SuggestBoardNameInput,\n @Ctx() context: ResolverContext\n ): Promise<string> {\n const { domain } = context.state\n const deps = this._buildSuggestDeps()\n return await suggestBoardName(input, { domain }, deps)\n }\n\n @Query(() => BoardMetaSuggestion, {\n description:\n '도메인 안에서 충돌하지 않는 Board name + 상세 description 제안. ' +\n 'name 은 짧고 심플 (30자 이내, 충돌 회피 (n) suffix), description 은 자세히 ' +\n '(사용자 prompt + AI importStrategy + 카테고리 분포 통계 합성).'\n })\n @Directive('@privilege(category: \"board-import\", privilege: \"query\")')\n async suggestBoardMeta(\n @Arg('input') input: SuggestBoardNameInput,\n @Ctx() context: ResolverContext\n ): Promise<SuggestBoardMetaResult> {\n const { domain } = context.state\n const deps = this._buildSuggestDeps()\n return await suggestBoardMeta(input, { domain }, deps)\n }\n\n /** 두 query 가 공유하는 deps wiring — typeorm find idiomatic 패턴 (distinctAlias 회귀 회피). */\n private _buildSuggestDeps() {\n const sessionRepo = getRepository(ImportSession) as any\n const attachmentRepo = getRepository(Attachment) as any\n const boardRepo: SuggestBoardRepoLike = {\n findExistingNames: async ({ domainId, basePrefix }) => {\n // 도메인 격리 + 'basePrefix' 로 시작하는 name 후보 한 번에 조회.\n // 단순 find({ where, select }) — take 없음 → DISTINCT 서브쿼리 미발생.\n const escaped = basePrefix.replace(/[%_\\\\]/g, ch => '\\\\' + ch)\n const items = await getRepository(Board).find({\n select: ['name'],\n where: { domain: { id: domainId }, name: Like(`${escaped}%`) }\n })\n return items.map(b => b.name as string)\n }\n }\n return { sessionRepo, attachmentRepo, boardRepo }\n }\n\n @Mutation(() => Board, {\n description:\n '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화한다. ' +\n '검수 단계 (사용자/AI 가 import 결과를 확인 후 명시적으로 호출) 에서 사용. ' +\n 'Board.state=\"draft\" 로 생성되므로 release mutation 으로 별도 발행 필요.'\n })\n @Directive('@privilege(category: \"board-import\", privilege: \"mutation\")')\n async materializeImportSession(\n @Arg('input') input: MaterializeImportSessionInput,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain, user } = context.state\n const sessionRepo = getRepository(ImportSession)\n const boardRepo = getRepository(Board)\n const groupRepo = getRepository(Group)\n\n const session = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } })\n\n return (await materializeFromSession(\n session as any,\n input,\n { domain, user },\n { boardRepo: boardRepo as any, groupRepo: groupRepo as any }\n )) as Board\n }\n}\n"]}
@@ -16,10 +16,10 @@ exports.runImportJob = runImportJob;
16
16
  const shell_1 = require("@things-factory/shell");
17
17
  const attachment_base_1 = require("@things-factory/attachment-base");
18
18
  const attachment_base_2 = require("@things-factory/attachment-base");
19
- const index_js_1 = require("../pipeline/index.js");
20
- const import_session_js_1 = require("./import-session.js");
19
+ const index_1 = require("../pipeline/index");
20
+ const import_session_1 = require("./import-session");
21
21
  async function runImportJob(sessionId) {
22
- const repo = (0, shell_1.getRepository)(import_session_js_1.ImportSession);
22
+ const repo = (0, shell_1.getRepository)(import_session_1.ImportSession);
23
23
  const session = await repo.findOneBy({ id: sessionId });
24
24
  if (!session) {
25
25
  console.error(`[board-import] ImportSession ${sessionId} not found`);
@@ -31,14 +31,23 @@ async function runImportJob(sessionId) {
31
31
  if (!attachment) {
32
32
  throw new Error(`Attachment ${session.attachmentId} not found`);
33
33
  }
34
- // attachment-base STORAGE.readFile 으로 본문 로드 (Buffer)
35
- const fileBuffer = attachment_base_2.STORAGE.readFile(attachment.path, null);
34
+ // attachment-base STORAGE.readFile 으로 본문 로드 (Buffer).
35
+ // file storage 는 sync, db/s3/azure 는 async — await 으로 양쪽 다 흡수.
36
+ const fileBuffer = (await attachment_base_2.STORAGE.readFile(attachment.path, null));
36
37
  await update(repo, sessionId, { status: 'parsing', progress: 20, message: 'Parsing drawing' });
37
38
  const opts = session.options || {};
38
39
  const filename = attachment.name || attachment.path;
39
40
  // importBoard 는 내부에서 stage 1~4 모두 수행. 단계별 progress 는 우선 거시적 업데이트.
40
41
  await update(repo, sessionId, { status: 'mapping', progress: 50, message: 'Mapping entities' });
41
- const result = await (0, index_js_1.importBoard)({
42
+ // userPrompt 는 parseOptions 로 합류 — image-adapter 가 VLM 프롬프트에 사용.
43
+ // 다른 adapter (DXF) 에는 무해 (몰라도 무시).
44
+ const mergedParseOptions = {
45
+ ...(opts.parseOptions || {}),
46
+ ...(opts.userPrompt ? { userPrompt: opts.userPrompt } : {})
47
+ };
48
+ // AI 가 같은 도면을 다른 관점으로 해석한 multi-variant. parse 한 번, 시안별 mapping + assemble.
49
+ // DXF 등 single-variant adapter 는 1 boardModel 만.
50
+ const result = await (0, index_1.importBoardVariants)({
42
51
  file: fileBuffer,
43
52
  filename,
44
53
  scopes: opts.scopes,
@@ -46,19 +55,52 @@ async function runImportJob(sessionId) {
46
55
  flipY: opts.flipY,
47
56
  scale: opts.scale,
48
57
  applyBinding: opts.applyBinding,
49
- parseOptions: opts.parseOptions
58
+ parseOptions: mergedParseOptions
50
59
  });
51
- await update(repo, sessionId, { status: 'assembling', progress: 80, message: 'Assembling board' });
60
+ await update(repo, sessionId, { status: 'assembling', progress: 80, message: 'Assembling boards' });
61
+ // 시안별 카테고리 분포 — 각 variant 의 entities 가 다르므로.
62
+ const variantsWithStats = result.variants.map((v, i) => {
63
+ const dist = {};
64
+ for (const c of v.boardModel.components ?? []) {
65
+ const k = c.category ?? 'unknown';
66
+ dist[k] = (dist[k] ?? 0) + 1;
67
+ }
68
+ return { variant: v, mapping: result.mappings[i], categoryDistribution: dist };
69
+ });
70
+ // default variantId — 첫 variant.
71
+ const defaultVariantId = result.variants[0]?.id;
72
+ // resultJson 구조:
73
+ // - variants: 풀 시안 데이터 (선택용 — boardModel + subject + importStrategy + stats + categoryDist)
74
+ // - boardModel: default variant 의 boardModel (back-compat)
75
+ // - defaultVariantId: 첫 variant 의 id
76
+ // - metadata: viewType / aiClientId / userPrompt 등 시안 무관 공통
77
+ const defaultVariant = variantsWithStats[0];
52
78
  const resultJson = JSON.stringify({
53
- boardModel: result.boardModel,
54
- stats: result.mapping.stats,
55
- warnings: result.mapping.warnings
79
+ variants: variantsWithStats.map(({ variant, mapping, categoryDistribution }) => ({
80
+ id: variant.id,
81
+ label: variant.label,
82
+ description: variant.description,
83
+ subjectName: variant.subjectName,
84
+ subjectDescription: variant.subjectDescription,
85
+ importStrategy: variant.importStrategy,
86
+ boardModel: variant.boardModel,
87
+ stats: mapping.stats,
88
+ warnings: mapping.warnings,
89
+ categoryDistribution
90
+ })),
91
+ defaultVariantId,
92
+ boardModel: defaultVariant?.variant.boardModel,
93
+ stats: defaultVariant?.mapping.stats,
94
+ warnings: defaultVariant?.mapping.warnings,
95
+ metadata: result.parse.metadata,
96
+ categoryDistribution: defaultVariant?.categoryDistribution
56
97
  });
98
+ const totalAcross = result.mappings.reduce((s, m) => s + (m.stats.total ?? 0), 0);
57
99
  await update(repo, sessionId, {
58
100
  status: 'completed',
59
101
  progress: 100,
60
- message: `Completed: ${result.mapping.stats.matched} matched, ${result.mapping.stats.generic} generic`,
61
- totalEntities: result.mapping.stats.total,
102
+ message: `Completed: ${result.variants.length} variants, ${totalAcross} entities total`,
103
+ totalEntities: defaultVariant?.mapping.stats.total ?? 0,
62
104
  resultJson,
63
105
  completedAt: new Date()
64
106
  });
@@ -1 +1 @@
1
- {"version":3,"file":"import-worker.js","sourceRoot":"","sources":["../../../server/service/import-session/import-worker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Background worker — ImportSession 처리.\n *\n * 흐름:\n * 1) ImportSession 로드 (status: queued)\n * 2) Attachment 본문 읽기 (attachment-base STORAGE.readFile)\n * 3) importBoard() 실행 (단계별로 status/progress 갱신)\n * 4) result 저장 + status='completed'\n * 5) 실패 시 status='failed' + message\n *\n * 단일 프로세스 내 setImmediate 기반. 추후 외부 큐(Redis/BullMQ) 로 이전 가능.\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport { Attachment } from '@things-factory/attachment-base'\nimport { STORAGE } from '@things-factory/attachment-base'\n\nimport { importBoard } from '../pipeline/index.js'\nimport { ImportSession } from './import-session.js'\n\nexport async function runImportJob(sessionId: string): Promise<void> {\n const repo = getRepository(ImportSession)\n const session = await repo.findOneBy({ id: sessionId })\n if (!session) {\n console.error(`[board-import] ImportSession ${sessionId} not found`)\n return\n }\n\n try {\n await update(repo, sessionId, { status: 'parsing', progress: 5, message: 'Reading attachment' })\n\n const attachment = await getRepository(Attachment).findOneBy({ id: session.attachmentId })\n if (!attachment) {\n throw new Error(`Attachment ${session.attachmentId} not found`)\n }\n\n // attachment-base STORAGE.readFile 으로 본문 로드 (Buffer)\n const fileBuffer = STORAGE.readFile(attachment.path!, null) as Buffer\n\n await update(repo, sessionId, { status: 'parsing', progress: 20, message: 'Parsing drawing' })\n\n const opts = session.options || {}\n const filename = attachment.name || attachment.path\n\n // importBoard 는 내부에서 stage 1~4 모두 수행. 단계별 progress 는 우선 거시적 업데이트.\n await update(repo, sessionId, { status: 'mapping', progress: 50, message: 'Mapping entities' })\n\n const result = await importBoard({\n file: fileBuffer,\n filename,\n scopes: opts.scopes,\n normalizeOrigin: opts.normalizeOrigin,\n flipY: opts.flipY,\n scale: opts.scale,\n applyBinding: opts.applyBinding,\n parseOptions: opts.parseOptions\n })\n\n await update(repo, sessionId, { status: 'assembling', progress: 80, message: 'Assembling board' })\n\n const resultJson = JSON.stringify({\n boardModel: result.boardModel,\n stats: result.mapping.stats,\n warnings: result.mapping.warnings\n })\n\n await update(repo, sessionId, {\n status: 'completed',\n progress: 100,\n message: `Completed: ${result.mapping.stats.matched} matched, ${result.mapping.stats.generic} generic`,\n totalEntities: result.mapping.stats.total,\n resultJson,\n completedAt: new Date()\n })\n } catch (err) {\n const message = (err as Error)?.message ?? String(err)\n console.error(`[board-import] ImportSession ${sessionId} failed:`, err)\n await update(repo, sessionId, {\n status: 'failed',\n message: `Failed: ${message}`,\n completedAt: new Date()\n })\n }\n}\n\nasync function update(repo: any, id: string, patch: Partial<ImportSession>): Promise<void> {\n await repo.update(id, patch as any)\n}\n"]}
1
+ {"version":3,"file":"import-worker.js","sourceRoot":"","sources":["../../../server/service/import-session/import-worker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Background worker — ImportSession 처리.\n *\n * 흐름:\n * 1) ImportSession 로드 (status: queued)\n * 2) Attachment 본문 읽기 (attachment-base STORAGE.readFile)\n * 3) importBoard() 실행 (단계별로 status/progress 갱신)\n * 4) result 저장 + status='completed'\n * 5) 실패 시 status='failed' + message\n *\n * 단일 프로세스 내 setImmediate 기반. 추후 외부 큐(Redis/BullMQ) 로 이전 가능.\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport { Attachment } from '@things-factory/attachment-base'\nimport { STORAGE } from '@things-factory/attachment-base'\n\nimport { importBoardVariants } from '../pipeline/index'\nimport { ImportSession } from './import-session'\n\nexport async function runImportJob(sessionId: string): Promise<void> {\n const repo = getRepository(ImportSession)\n const session = await repo.findOneBy({ id: sessionId })\n if (!session) {\n console.error(`[board-import] ImportSession ${sessionId} not found`)\n return\n }\n\n try {\n await update(repo, sessionId, { status: 'parsing', progress: 5, message: 'Reading attachment' })\n\n const attachment = await getRepository(Attachment).findOneBy({ id: session.attachmentId })\n if (!attachment) {\n throw new Error(`Attachment ${session.attachmentId} not found`)\n }\n\n // attachment-base STORAGE.readFile 으로 본문 로드 (Buffer).\n // file storage 는 sync, db/s3/azure 는 async — await 으로 양쪽 다 흡수.\n const fileBuffer = (await STORAGE.readFile(attachment.path!, null)) as Buffer\n\n await update(repo, sessionId, { status: 'parsing', progress: 20, message: 'Parsing drawing' })\n\n const opts = session.options || {}\n const filename = attachment.name || attachment.path\n\n // importBoard 는 내부에서 stage 1~4 모두 수행. 단계별 progress 는 우선 거시적 업데이트.\n await update(repo, sessionId, { status: 'mapping', progress: 50, message: 'Mapping entities' })\n\n // userPrompt 는 parseOptions 로 합류 — image-adapter 가 VLM 프롬프트에 사용.\n // 다른 adapter (DXF) 에는 무해 (몰라도 무시).\n const mergedParseOptions = {\n ...(opts.parseOptions || {}),\n ...(opts.userPrompt ? { userPrompt: opts.userPrompt } : {})\n }\n // AI 가 같은 도면을 다른 관점으로 해석한 multi-variant. parse 한 번, 시안별 mapping + assemble.\n // DXF 등 single-variant adapter 는 1 boardModel 만.\n const result = await importBoardVariants({\n file: fileBuffer,\n filename,\n scopes: opts.scopes,\n normalizeOrigin: opts.normalizeOrigin,\n flipY: opts.flipY,\n scale: opts.scale,\n applyBinding: opts.applyBinding,\n parseOptions: mergedParseOptions\n })\n\n await update(repo, sessionId, { status: 'assembling', progress: 80, message: 'Assembling boards' })\n\n // 시안별 카테고리 분포 — 각 variant 의 entities 가 다르므로.\n const variantsWithStats = result.variants.map((v, i) => {\n const dist: Record<string, number> = {}\n for (const c of v.boardModel.components ?? []) {\n const k = c.category ?? 'unknown'\n dist[k] = (dist[k] ?? 0) + 1\n }\n return { variant: v, mapping: result.mappings[i], categoryDistribution: dist }\n })\n\n // default variantId — 첫 variant.\n const defaultVariantId = result.variants[0]?.id\n\n // resultJson 구조:\n // - variants: 풀 시안 데이터 (선택용 — boardModel + subject + importStrategy + stats + categoryDist)\n // - boardModel: default variant 의 boardModel (back-compat)\n // - defaultVariantId: 첫 variant 의 id\n // - metadata: viewType / aiClientId / userPrompt 등 시안 무관 공통\n const defaultVariant = variantsWithStats[0]\n const resultJson = JSON.stringify({\n variants: variantsWithStats.map(({ variant, mapping, categoryDistribution }) => ({\n id: variant.id,\n label: variant.label,\n description: variant.description,\n subjectName: variant.subjectName,\n subjectDescription: variant.subjectDescription,\n importStrategy: variant.importStrategy,\n boardModel: variant.boardModel,\n stats: mapping.stats,\n warnings: mapping.warnings,\n categoryDistribution\n })),\n defaultVariantId,\n boardModel: defaultVariant?.variant.boardModel,\n stats: defaultVariant?.mapping.stats,\n warnings: defaultVariant?.mapping.warnings,\n metadata: result.parse.metadata,\n categoryDistribution: defaultVariant?.categoryDistribution\n })\n\n const totalAcross = result.mappings.reduce((s, m) => s + (m.stats.total ?? 0), 0)\n await update(repo, sessionId, {\n status: 'completed',\n progress: 100,\n message: `Completed: ${result.variants.length} variants, ${totalAcross} entities total`,\n totalEntities: defaultVariant?.mapping.stats.total ?? 0,\n resultJson,\n completedAt: new Date()\n })\n } catch (err) {\n const message = (err as Error)?.message ?? String(err)\n console.error(`[board-import] ImportSession ${sessionId} failed:`, err)\n await update(repo, sessionId, {\n status: 'failed',\n message: `Failed: ${message}`,\n completedAt: new Date()\n })\n }\n}\n\nasync function update(repo: any, id: string, patch: Partial<ImportSession>): Promise<void> {\n await repo.update(id, patch as any)\n}\n"]}
@@ -1,6 +1,8 @@
1
- import { ImportSession } from './import-session.js';
2
- import { ImportSessionResolver } from './import-session-resolver.js';
1
+ import { ImportSession } from './import-session';
2
+ import { ImportSessionResolver } from './import-session-resolver';
3
3
  export { ImportSession, ImportSessionResolver };
4
- export { runImportJob } from './import-worker.js';
4
+ export { runImportJob } from './import-worker';
5
+ export { startImportJobAsync, fetchImportSession, listImportSessionsByChatSession, describeAdapter, type StartImportJobInput, type ImportActionsContext, type SessionRepoLike, type AdapterInfo } from './import-actions';
6
+ export { materializeFromSession, type MaterializeRequest, type MaterializeContext, type MaterializeDeps } from './materialize-from-session';
5
7
  export declare const entities: (typeof ImportSession)[];
6
8
  export declare const resolverClasses: (typeof ImportSessionResolver)[];
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolverClasses = exports.entities = exports.runImportJob = exports.ImportSessionResolver = exports.ImportSession = void 0;
4
- const import_session_js_1 = require("./import-session.js");
5
- Object.defineProperty(exports, "ImportSession", { enumerable: true, get: function () { return import_session_js_1.ImportSession; } });
6
- const import_session_resolver_js_1 = require("./import-session-resolver.js");
7
- Object.defineProperty(exports, "ImportSessionResolver", { enumerable: true, get: function () { return import_session_resolver_js_1.ImportSessionResolver; } });
8
- var import_worker_js_1 = require("./import-worker.js");
9
- Object.defineProperty(exports, "runImportJob", { enumerable: true, get: function () { return import_worker_js_1.runImportJob; } });
10
- exports.entities = [import_session_js_1.ImportSession];
11
- exports.resolverClasses = [import_session_resolver_js_1.ImportSessionResolver];
3
+ exports.resolverClasses = exports.entities = exports.materializeFromSession = exports.describeAdapter = exports.listImportSessionsByChatSession = exports.fetchImportSession = exports.startImportJobAsync = exports.runImportJob = exports.ImportSessionResolver = exports.ImportSession = void 0;
4
+ const import_session_1 = require("./import-session");
5
+ Object.defineProperty(exports, "ImportSession", { enumerable: true, get: function () { return import_session_1.ImportSession; } });
6
+ const import_session_resolver_1 = require("./import-session-resolver");
7
+ Object.defineProperty(exports, "ImportSessionResolver", { enumerable: true, get: function () { return import_session_resolver_1.ImportSessionResolver; } });
8
+ var import_worker_1 = require("./import-worker");
9
+ Object.defineProperty(exports, "runImportJob", { enumerable: true, get: function () { return import_worker_1.runImportJob; } });
10
+ var import_actions_1 = require("./import-actions");
11
+ Object.defineProperty(exports, "startImportJobAsync", { enumerable: true, get: function () { return import_actions_1.startImportJobAsync; } });
12
+ Object.defineProperty(exports, "fetchImportSession", { enumerable: true, get: function () { return import_actions_1.fetchImportSession; } });
13
+ Object.defineProperty(exports, "listImportSessionsByChatSession", { enumerable: true, get: function () { return import_actions_1.listImportSessionsByChatSession; } });
14
+ Object.defineProperty(exports, "describeAdapter", { enumerable: true, get: function () { return import_actions_1.describeAdapter; } });
15
+ var materialize_from_session_1 = require("./materialize-from-session");
16
+ Object.defineProperty(exports, "materializeFromSession", { enumerable: true, get: function () { return materialize_from_session_1.materializeFromSession; } });
17
+ exports.entities = [import_session_1.ImportSession];
18
+ exports.resolverClasses = [import_session_resolver_1.ImportSessionResolver];
12
19
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/import-session/index.ts"],"names":[],"mappings":";;;AAAA,2DAAmD;AAG1C,8FAHA,iCAAa,OAGA;AAFtB,6EAAoE;AAE5C,sGAFf,kDAAqB,OAEe;AAC7C,uDAAiD;AAAxC,gHAAA,YAAY,OAAA;AAER,QAAA,QAAQ,GAAG,CAAC,iCAAa,CAAC,CAAA;AAC1B,QAAA,eAAe,GAAG,CAAC,kDAAqB,CAAC,CAAA","sourcesContent":["import { ImportSession } from './import-session.js'\nimport { ImportSessionResolver } from './import-session-resolver.js'\n\nexport { ImportSession, ImportSessionResolver }\nexport { runImportJob } from './import-worker.js'\n\nexport const entities = [ImportSession]\nexport const resolverClasses = [ImportSessionResolver]\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/import-session/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAGvC,8FAHA,8BAAa,OAGA;AAFtB,uEAAiE;AAEzC,sGAFf,+CAAqB,OAEe;AAC7C,iDAA8C;AAArC,6GAAA,YAAY,OAAA;AACrB,mDASyB;AARvB,qHAAA,mBAAmB,OAAA;AACnB,oHAAA,kBAAkB,OAAA;AAClB,iIAAA,+BAA+B,OAAA;AAC/B,iHAAA,eAAe,OAAA;AAMjB,uEAKmC;AAJjC,kIAAA,sBAAsB,OAAA;AAMX,QAAA,QAAQ,GAAG,CAAC,8BAAa,CAAC,CAAA;AAC1B,QAAA,eAAe,GAAG,CAAC,+CAAqB,CAAC,CAAA","sourcesContent":["import { ImportSession } from './import-session'\nimport { ImportSessionResolver } from './import-session-resolver'\n\nexport { ImportSession, ImportSessionResolver }\nexport { runImportJob } from './import-worker'\nexport {\n startImportJobAsync,\n fetchImportSession,\n listImportSessionsByChatSession,\n describeAdapter,\n type StartImportJobInput,\n type ImportActionsContext,\n type SessionRepoLike,\n type AdapterInfo\n} from './import-actions'\nexport {\n materializeFromSession,\n type MaterializeRequest,\n type MaterializeContext,\n type MaterializeDeps\n} from './materialize-from-session'\n\nexport const entities = [ImportSession]\nexport const resolverClasses = [ImportSessionResolver]\n"]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * materializeFromSession — ImportSession.result.boardModel → 신규 Board 영속화의 **순수 비즈니스
3
+ * 로직**. typeorm `getRepository` / `ResolverContext` 같은 외부 의존성을 인자로 분리해서 단위
4
+ * 테스트 가능하도록 구성. resolver 측 mutation 은 IO (repo, ctx) 만 wiring 해서 이 함수를 호출.
5
+ *
6
+ * 호출 책임:
7
+ * - resolver: getRepository / context.state 에서 도메인/유저 추출
8
+ * - 이 함수: 검증 → 정책 적용 → save 호출 → 결과 반환
9
+ *
10
+ * 정책:
11
+ * - ImportSession.status 가 'completed' 가 아니면 거절
12
+ * - result.boardModel 이 없으면 거절
13
+ * - Board.name 이 같은 도메인에 이미 있으면 거절 (createBoard 와 동일 정책)
14
+ * - 새 보드는 sortOrder = (현재 최소 sortOrder - 1) 또는 0 (createBoard 와 동일 정책)
15
+ * - state='draft', sourceImportSessionId=session.id, model=JSON.stringify(boardModel)
16
+ */
17
+ export interface MaterializeRequest {
18
+ sessionId: string;
19
+ name: string;
20
+ description?: string;
21
+ groupId?: string;
22
+ type?: 'main' | 'sub' | 'popup';
23
+ thumbnail?: string;
24
+ /** 사용자가 선택한 시안 id. 미지정 시 session.result.defaultVariantId 또는 첫 variant. */
25
+ variantId?: string;
26
+ }
27
+ export interface SessionLike {
28
+ id: string;
29
+ status: string;
30
+ result?: {
31
+ boardModel?: any;
32
+ variants?: Array<{
33
+ id: string;
34
+ boardModel: any;
35
+ }>;
36
+ defaultVariantId?: string;
37
+ } | null;
38
+ }
39
+ export interface BoardRepoLike {
40
+ findOneBy(criteria: any): Promise<any | null>;
41
+ save(entity: any): Promise<any>;
42
+ /** sortOrder 결정용 — 미존재 시 0 으로 fallback. */
43
+ createQueryBuilder?(alias: string): {
44
+ select(expr: string, alias: string): any;
45
+ where(cond: string, params: any): any;
46
+ getRawOne(): Promise<any>;
47
+ };
48
+ }
49
+ export interface GroupRepoLike {
50
+ findOneBy(criteria: any): Promise<any | null>;
51
+ }
52
+ export interface MaterializeContext {
53
+ domain: {
54
+ id: string;
55
+ [k: string]: any;
56
+ };
57
+ user: any;
58
+ }
59
+ export interface MaterializeDeps {
60
+ boardRepo: BoardRepoLike;
61
+ groupRepo: GroupRepoLike;
62
+ }
63
+ /**
64
+ * ImportSession 결과를 새 Board 로 저장. 검증 실패 시 throw, 성공 시 저장된 Board 반환.
65
+ */
66
+ export declare function materializeFromSession(session: SessionLike | null, input: MaterializeRequest, context: MaterializeContext, deps: MaterializeDeps): Promise<any>;