@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.
- package/dist-server/index.d.ts +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/adapters/dxf-adapter.d.ts +1 -1
- package/dist-server/service/adapters/dxf-adapter.js +18 -4
- package/dist-server/service/adapters/dxf-adapter.js.map +1 -1
- package/dist-server/service/adapters/image-adapter.d.ts +32 -0
- package/dist-server/service/adapters/image-adapter.js +420 -0
- package/dist-server/service/adapters/image-adapter.js.map +1 -0
- package/dist-server/service/ai/types.d.ts +96 -4
- package/dist-server/service/ai/types.js +221 -12
- package/dist-server/service/ai/types.js.map +1 -1
- package/dist-server/service/converters/generic-to.d.ts +17 -8
- package/dist-server/service/converters/generic-to.js +44 -44
- package/dist-server/service/converters/generic-to.js.map +1 -1
- package/dist-server/service/import-session/import-actions.d.ts +79 -0
- package/dist-server/service/import-session/import-actions.js +98 -0
- package/dist-server/service/import-session/import-actions.js.map +1 -0
- package/dist-server/service/import-session/import-session-resolver.d.ts +22 -1
- package/dist-server/service/import-session/import-session-resolver.js +166 -38
- package/dist-server/service/import-session/import-session-resolver.js.map +1 -1
- package/dist-server/service/import-session/import-worker.js +55 -13
- package/dist-server/service/import-session/import-worker.js.map +1 -1
- package/dist-server/service/import-session/index.d.ts +5 -3
- package/dist-server/service/import-session/index.js +16 -9
- package/dist-server/service/import-session/index.js.map +1 -1
- package/dist-server/service/import-session/materialize-from-session.d.ts +66 -0
- package/dist-server/service/import-session/materialize-from-session.js +104 -0
- package/dist-server/service/import-session/materialize-from-session.js.map +1 -0
- package/dist-server/service/import-session/suggest-board-name.d.ts +80 -0
- package/dist-server/service/import-session/suggest-board-name.js +195 -0
- package/dist-server/service/import-session/suggest-board-name.js.map +1 -0
- package/dist-server/service/import-tools.d.ts +10 -0
- package/dist-server/service/import-tools.js +227 -0
- package/dist-server/service/import-tools.js.map +1 -0
- package/dist-server/service/index.d.ts +22 -19
- package/dist-server/service/index.js +46 -36
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/pipeline/index.d.ts +42 -9
- package/dist-server/service/pipeline/index.js +92 -23
- package/dist-server/service/pipeline/index.js.map +1 -1
- package/dist-server/service/pipeline/stage2-mapping.d.ts +20 -5
- package/dist-server/service/pipeline/stage2-mapping.js +84 -26
- package/dist-server/service/pipeline/stage2-mapping.js.map +1 -1
- package/dist-server/service/pipeline/stage3-board.d.ts +2 -2
- package/dist-server/service/pipeline/stage3-board.js +65 -10
- package/dist-server/service/pipeline/stage3-board.js.map +1 -1
- package/dist-server/service/pipeline/stage4-binding.d.ts +1 -1
- package/dist-server/service/pipeline/stage4-binding.js +2 -2
- package/dist-server/service/pipeline/stage4-binding.js.map +1 -1
- package/dist-server/service/registry/index.d.ts +1 -1
- package/dist-server/service/registry/index.js.map +1 -1
- package/dist-server/service/types/index.d.ts +35 -0
- package/dist-server/service/types/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/server/index.ts +1 -1
- package/server/service/adapters/dxf-adapter.ts +17 -4
- package/server/service/adapters/image-adapter.test.ts +545 -0
- package/server/service/adapters/image-adapter.ts +464 -0
- package/server/service/ai/types.ts +354 -19
- package/server/service/converters/generic-to.test.ts +91 -0
- package/server/service/converters/generic-to.ts +66 -49
- package/server/service/import-session/import-actions.test.ts +185 -0
- package/server/service/import-session/import-actions.ts +164 -0
- package/server/service/import-session/import-session-resolver.ts +171 -39
- package/server/service/import-session/import-worker.ts +56 -12
- package/server/service/import-session/index.ts +19 -3
- package/server/service/import-session/materialize-from-session.test.ts +274 -0
- package/server/service/import-session/materialize-from-session.ts +158 -0
- package/server/service/import-session/suggest-board-name.test.ts +271 -0
- package/server/service/import-session/suggest-board-name.ts +279 -0
- package/server/service/import-tools.test.ts +137 -0
- package/server/service/import-tools.ts +255 -0
- package/server/service/index.ts +35 -18
- package/server/service/pipeline/index.ts +118 -23
- package/server/service/pipeline/stage2-mapping.test.ts +204 -0
- package/server/service/pipeline/stage2-mapping.ts +102 -27
- package/server/service/pipeline/stage3-board.test.ts +133 -0
- package/server/service/pipeline/stage3-board.ts +76 -11
- package/server/service/pipeline/stage4-binding.ts +2 -2
- package/server/service/registry/index.ts +1 -1
- 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
|
|
21
|
-
const
|
|
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
|
|
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)(
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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)(() =>
|
|
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)(() => [
|
|
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)(() =>
|
|
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
|
|
20
|
-
const
|
|
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)(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
58
|
+
parseOptions: mergedParseOptions
|
|
50
59
|
});
|
|
51
|
-
await update(repo, sessionId, { status: 'assembling', progress: 80, message: 'Assembling
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
61
|
-
totalEntities:
|
|
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":";;AAoBA,
|
|
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
|
|
2
|
-
import { ImportSessionResolver } from './import-session-resolver
|
|
1
|
+
import { ImportSession } from './import-session';
|
|
2
|
+
import { ImportSessionResolver } from './import-session-resolver';
|
|
3
3
|
export { ImportSession, ImportSessionResolver };
|
|
4
|
-
export { runImportJob } from './import-worker
|
|
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
|
|
5
|
-
Object.defineProperty(exports, "ImportSession", { enumerable: true, get: function () { return
|
|
6
|
-
const
|
|
7
|
-
Object.defineProperty(exports, "ImportSessionResolver", { enumerable: true, get: function () { return
|
|
8
|
-
var
|
|
9
|
-
Object.defineProperty(exports, "runImportJob", { enumerable: true, get: function () { return
|
|
10
|
-
|
|
11
|
-
exports
|
|
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,
|
|
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>;
|