@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
@@ -1,43 +1,74 @@
1
1
  /**
2
- * GenericObject ↔ 도메인 컴포넌트 변환.
2
+ * Generic placeholder → 도메인 컴포넌트 변환.
3
3
  *
4
- * Import 시 매칭되지 않은 entity 는 GenericObject 로 보드에 배치된다.
5
- * 사용자가 사후에 모델러 UI 에서 "Convert to Stocker" 등 액션으로 호출.
4
+ * Import 시 매칭되지 않은 entity 는 `placeholder` 타입으로 보드에 배치된다.
5
+ * 사용자가 사후에 모델러 UI 에서 "Convert to ..." 액션으로 도메인 컴포넌트로 변환.
6
+ *
7
+ * 아키텍처 원칙:
8
+ * - `placeholder` — 도메인 비종속 임시 추상. 다른 type 으로 변환하는 기능만.
9
+ * GLB 지원 없음 (GltfComponent mixin 제거됨).
10
+ * - 도메인 generic 추상 (GenericTransport, GenericFacility 등) — 영구 도메인 추상.
11
+ * GLB 바디를 직접 채우는 방식으로 사용. 변환 대상 아님.
6
12
  *
7
13
  * 변환 시:
8
14
  * - 좌표/크기/회전 보존
9
15
  * - source (원본 entity attribs) 보존
10
16
  * - 등록된 ImportRule.build 의 defaultState / idStrategy 적용
11
17
  */
12
- import { find3DPreset, getImportRules } from '../registry/index.js'
13
- import type { BoardComponent } from '../pipeline/stage3-board.js'
14
- import type { ComponentCategory } from '../types/registry.js'
15
- import { GENERIC_OBJECT_TYPE } from '../pipeline/stage2-mapping.js'
18
+ import { find3DPreset, getImportRules } from '../registry/index'
19
+ import type { BoardComponent } from '../pipeline/stage3-board'
20
+ import type { ComponentCategory } from '../types/registry'
21
+ import { GENERIC_OBJECT_TYPE } from '../pipeline/stage2-mapping'
16
22
 
17
23
  export interface ConvertOptions {
18
24
  scopes?: string[]
19
- /** 새 컴포넌트의 ID. 미지정 시 ImportRule 의 idStrategy 적용 또는 자동 생성 */
25
+ /** 새 컴포넌트의 ID. 미지정 시 ImportRule 의 idStrategy 적용 또는 자동 생성. */
20
26
  id?: string
27
+ /**
28
+ * 코어 placeholder type ('placeholder') 외에 인정할 도메인 추상 type list.
29
+ * 예: ['GenericTransport', 'GenericFacility'].
30
+ * 호출자가 자기 도메인 패키지의 추상까지 변환 대상으로 포함하도록 명시.
31
+ */
32
+ extraPlaceholderTypes?: ReadonlyArray<string>
33
+ }
34
+
35
+ /** things-scene 코어가 등록한 도메인 비종속 placeholder type. */
36
+ export const CORE_PLACEHOLDER_TYPES = [GENERIC_OBJECT_TYPE] as const
37
+
38
+ export function isPlaceholderType(
39
+ type: string | undefined,
40
+ extraTypes: ReadonlyArray<string> = []
41
+ ): boolean {
42
+ if (!type) return false
43
+ if ((CORE_PLACEHOLDER_TYPES as readonly string[]).includes(type)) return true
44
+ return extraTypes.includes(type)
21
45
  }
22
46
 
47
+ // ── 일반화된 변환 helper ─────────────────────────────────────────────
48
+
23
49
  /**
24
- * GenericObject 를 지정된 도메인 type 으로 변환한다.
25
- * 기존 좌표/크기/회전/source 보존.
50
+ * 추상 placeholder 를 지정된 도메인 type 으로 변환. 호환되는 모든 generic-* 수용 (코어 + 도메인).
51
+ *
52
+ * 기존 좌표/크기/회전/source 보존. placeholder 시각 메타 (placeholderLabel/Hint, fillStyle 등) 는 제거.
26
53
  */
27
- export function convertGenericTo(
28
- generic: BoardComponent,
54
+ export function convertPlaceholderTo(
55
+ placeholder: BoardComponent,
29
56
  toType: string,
30
57
  options: ConvertOptions = {}
31
58
  ): BoardComponent {
32
- if (generic.type !== GENERIC_OBJECT_TYPE) {
33
- throw new Error(`Source component is not GenericObject (type=${generic.type})`)
59
+ const extra = options.extraPlaceholderTypes ?? []
60
+ if (!isPlaceholderType(placeholder.type, extra)) {
61
+ throw new Error(
62
+ `Source component is not a generic placeholder (type=${placeholder.type}). ` +
63
+ `Expected one of: ${[...CORE_PLACEHOLDER_TYPES, ...extra].join(', ')}`
64
+ )
34
65
  }
35
66
 
36
67
  const rules = getImportRules(options.scopes)
37
68
  const rule = rules.find(r => r.componentType === toType)
38
69
 
39
70
  const category: ComponentCategory = rule?.category ?? 'unknown'
40
- const newId = options.id ?? generic.id
71
+ const newId = options.id ?? placeholder.id
41
72
 
42
73
  const preset = find3DPreset(toType, category, options.scopes)
43
74
  const threeD = {
@@ -46,7 +77,16 @@ export function convertGenericTo(
46
77
  origin: preset.origin ?? 'bottom'
47
78
  }
48
79
 
49
- const { strokeStyle, strokeDashArray, fillStyle, label, ...preservedState } = generic
80
+ // placeholder 시각/스타일 메타는 새 type 으로 옮길 때 의미 없음 — 제거.
81
+ const {
82
+ strokeStyle: _strokeStyle,
83
+ strokeDashArray: _strokeDashArray,
84
+ fillStyle: _fillStyle,
85
+ label: _label,
86
+ placeholderLabel: _placeholderLabel,
87
+ placeholderHint: _placeholderHint,
88
+ ...preservedState
89
+ } = placeholder as any
50
90
 
51
91
  return {
52
92
  ...preservedState,
@@ -54,45 +94,22 @@ export function convertGenericTo(
54
94
  type: toType,
55
95
  id: newId,
56
96
  threeD,
57
- ...(rule?.build.defaultState || {}),
58
- /**
59
- * 변환 추적 — 되돌리기 가능하게 source 보존.
60
- * `convertedFrom` 키는 board-modeller UI 에서 "되돌리기" 액션 활성화 단서.
61
- */
62
- convertedFrom: {
63
- type: GENERIC_OBJECT_TYPE,
64
- previousState: { strokeStyle, strokeDashArray, fillStyle, label }
65
- }
97
+ ...(rule?.build.defaultState || {})
66
98
  }
67
99
  }
68
100
 
101
+ // ── Backward-compat alias — 기존 호출처 호환 ──────────────────────
102
+
69
103
  /**
70
- * 변환된 컴포넌트를 다시 GenericObject 로 되돌린다.
71
- * convertedFrom 정보가 있어야 가능.
104
+ * @deprecated Use `convertPlaceholderTo`. Placeholder 한정 — 새 코드는 일반화된 helper.
72
105
  */
73
- export function revertToGeneric(
74
- component: BoardComponent,
106
+ export function convertGenericTo(
107
+ generic: BoardComponent,
108
+ toType: string,
75
109
  options: ConvertOptions = {}
76
110
  ): BoardComponent {
77
- const previous = (component as any).convertedFrom?.previousState
78
- if (!previous) {
79
- throw new Error('이 컴포넌트는 GenericObject 에서 변환된 것이 아닙니다 (convertedFrom 없음).')
80
- }
81
-
82
- const preset = find3DPreset(GENERIC_OBJECT_TYPE, 'unknown', options.scopes)
83
- const threeD = {
84
- geometry: preset.geometry,
85
- ...(preset.material && { material: preset.material }),
86
- origin: preset.origin ?? 'bottom'
87
- }
88
-
89
- const { convertedFrom, ...rest } = component as any
90
-
91
- return {
92
- ...rest,
93
- category: 'unknown',
94
- type: GENERIC_OBJECT_TYPE,
95
- threeD,
96
- ...previous
111
+ if (generic.type !== GENERIC_OBJECT_TYPE) {
112
+ throw new Error(`Source component is not Placeholder (type=${generic.type})`)
97
113
  }
114
+ return convertPlaceholderTo(generic, toType, options)
98
115
  }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * import-actions — 단위 테스트.
3
+ *
4
+ * ResolverContext 무관 service 함수의 동작 검증. mock SessionRepoLike 로 격리.
5
+ * 비동기 worker (runImportJob) 는 setImmediate side-effect 로 분리되어 있어
6
+ * 본 테스트 범위 밖 — startImportJobAsync 는 session 저장 후 즉시 return 만 검증.
7
+ */
8
+ import {
9
+ startImportJobAsync,
10
+ fetchImportSession,
11
+ listImportSessionsByChatSession,
12
+ describeAdapter,
13
+ type ImportActionsContext,
14
+ type SessionRepoLike
15
+ } from './import-actions'
16
+
17
+ const DOMAIN = { id: 'domain-1', name: 'test' }
18
+ const USER = { id: 'user-1' }
19
+ const CTX: ImportActionsContext = { domain: DOMAIN, user: USER }
20
+
21
+ function makeRepo(overrides: Partial<SessionRepoLike> = {}) {
22
+ const findOneByCalls: any[] = []
23
+ const saveCalls: any[] = []
24
+ const findCalls: any[] = []
25
+ const repo: any = {
26
+ findOneByCalls,
27
+ saveCalls,
28
+ findCalls,
29
+ findOneBy: async (criteria: any) => {
30
+ findOneByCalls.push(criteria)
31
+ return null
32
+ },
33
+ save: async (entity: any) => {
34
+ saveCalls.push(entity)
35
+ return { ...entity, id: 'session-' + (saveCalls.length) }
36
+ },
37
+ find: async (options: any) => {
38
+ findCalls.push(options)
39
+ return []
40
+ },
41
+ ...overrides
42
+ }
43
+ return repo
44
+ }
45
+
46
+ // ── startImportJobAsync ────────────────────────────────────────────
47
+
48
+ describe('startImportJobAsync', () => {
49
+ test('session 을 status=queued / progress=0 으로 저장', async () => {
50
+ const repo = makeRepo()
51
+ await startImportJobAsync({ attachmentId: 'a1' }, CTX, repo, () => {})
52
+ expect(repo.saveCalls).toHaveLength(1)
53
+ expect(repo.saveCalls[0]).toMatchObject({
54
+ domain: DOMAIN,
55
+ attachmentId: 'a1',
56
+ status: 'queued',
57
+ progress: 0,
58
+ message: 'Queued',
59
+ creator: USER
60
+ })
61
+ })
62
+
63
+ test('options 가 optionsJson 으로 직렬화', async () => {
64
+ const repo = makeRepo()
65
+ await startImportJobAsync(
66
+ {
67
+ attachmentId: 'a1',
68
+ scopes: ['fmsim'],
69
+ normalizeOrigin: true,
70
+ flipY: false,
71
+ scale: 2,
72
+ applyBinding: false,
73
+ parseOptions: { excludeLayers: ['ANNO'] }
74
+ },
75
+ CTX,
76
+ repo,
77
+ () => {}
78
+ )
79
+ const json = JSON.parse(repo.saveCalls[0].optionsJson)
80
+ expect(json).toEqual({
81
+ scopes: ['fmsim'],
82
+ normalizeOrigin: true,
83
+ flipY: false,
84
+ scale: 2,
85
+ applyBinding: false,
86
+ parseOptions: { excludeLayers: ['ANNO'] }
87
+ })
88
+ })
89
+
90
+ test('chatSessionId 가 옵션이라 미지정 시 undefined 로 저장', async () => {
91
+ const repo = makeRepo()
92
+ await startImportJobAsync({ attachmentId: 'a1' }, CTX, repo, () => {})
93
+ expect(repo.saveCalls[0].chatSessionId).toBeUndefined()
94
+ })
95
+ })
96
+
97
+ // ── fetchImportSession ────────────────────────────────────────────
98
+
99
+ describe('fetchImportSession', () => {
100
+ test('domain id 기반 격리 조회', async () => {
101
+ const repo = makeRepo()
102
+ await fetchImportSession('s1', CTX, repo)
103
+ expect(repo.findOneByCalls[0]).toEqual({
104
+ id: 's1',
105
+ domain: { id: DOMAIN.id }
106
+ })
107
+ })
108
+
109
+ test('미존재 시 null', async () => {
110
+ const repo = makeRepo()
111
+ const r = await fetchImportSession('s1', CTX, repo)
112
+ expect(r).toBeNull()
113
+ })
114
+
115
+ test('존재 시 session 그대로 반환', async () => {
116
+ const session = { id: 's1', status: 'completed' } as any
117
+ const repo = makeRepo({ findOneBy: async () => session })
118
+ const r = await fetchImportSession('s1', CTX, repo)
119
+ expect(r).toBe(session)
120
+ })
121
+ })
122
+
123
+ // ── listImportSessionsByChatSession ────────────────────────────────
124
+
125
+ describe('listImportSessionsByChatSession', () => {
126
+ test('chatSessionId + domain 기반, 최신순 50개', async () => {
127
+ const repo = makeRepo()
128
+ await listImportSessionsByChatSession('chat-1', CTX, repo)
129
+ expect(repo.findCalls[0]).toEqual({
130
+ where: { chatSessionId: 'chat-1', domain: { id: DOMAIN.id } },
131
+ order: { createdAt: 'DESC' },
132
+ take: 50
133
+ })
134
+ })
135
+ })
136
+
137
+ // ── describeAdapter ───────────────────────────────────────────────
138
+
139
+ describe('describeAdapter', () => {
140
+ test('FormatAdapter 의 능력을 직렬화 가능 평면 객체로', () => {
141
+ const adapter = {
142
+ format: 'dxf',
143
+ supportedExtensions: ['.dxf'],
144
+ capabilities: {
145
+ has2D: true,
146
+ has3D: false,
147
+ hasSemanticLabels: false,
148
+ hasTextLabels: true,
149
+ hasAttributes: true
150
+ }
151
+ }
152
+ expect(describeAdapter(adapter)).toEqual({
153
+ format: 'dxf',
154
+ supportedExtensions: ['.dxf'],
155
+ capabilities: {
156
+ has2D: true,
157
+ has3D: false,
158
+ hasSemanticLabels: false,
159
+ hasTextLabels: true,
160
+ hasAttributes: true
161
+ }
162
+ })
163
+ })
164
+
165
+ test('falsy capability 도 false 로 정규화', () => {
166
+ const adapter = {
167
+ format: 'image',
168
+ supportedExtensions: ['.png'],
169
+ capabilities: {
170
+ has2D: 1, // truthy non-bool
171
+ has3D: undefined,
172
+ hasSemanticLabels: 'yes',
173
+ hasTextLabels: 0,
174
+ hasAttributes: null
175
+ }
176
+ }
177
+ expect(describeAdapter(adapter as any).capabilities).toEqual({
178
+ has2D: true,
179
+ has3D: false,
180
+ hasSemanticLabels: true,
181
+ hasTextLabels: false,
182
+ hasAttributes: false
183
+ })
184
+ })
185
+ })
@@ -0,0 +1,164 @@
1
+ /**
2
+ * board-import 의 ResolverContext 무관 service-level 함수.
3
+ *
4
+ * import-session-resolver 의 mutation/query 핵심 로직을 typeorm getRepository /
5
+ * ResolverContext 외부 의존성을 인자로 분리해 단위 테스트 + LLM tool builder 에서
6
+ * 재사용 가능하도록 추출. materializeFromSession 의 패턴 동일.
7
+ *
8
+ * 호출자 책임:
9
+ * - resolver: getRepository / context.state 에서 도메인/유저 추출 후 deps 채워 호출
10
+ * - import-tools.ts: ToolDispatchHelpers 가 builder 호출 시 ctx 에서 추출
11
+ */
12
+
13
+ // type-only import — module level 에서 typeorm decorator chain 을 평가하지 않는다.
14
+ // 단위 테스트 환경이 typeorm DataSource init 전에도 service 함수 검증할 수 있게 함.
15
+ import type { ImportSession } from './import-session'
16
+
17
+ // ── 입력/의존성 타입 ──────────────────────────────────────────────────
18
+
19
+ export interface SessionRepoLike {
20
+ findOneBy(criteria: any): Promise<ImportSession | null>
21
+ save(entity: any): Promise<ImportSession>
22
+ find(options: any): Promise<ImportSession[]>
23
+ }
24
+
25
+ export interface ImportActionsContext {
26
+ domain: { id: string; [k: string]: any }
27
+ user: any
28
+ }
29
+
30
+ export interface StartImportJobInput {
31
+ attachmentId: string
32
+ chatSessionId?: string
33
+ scopes?: string[]
34
+ normalizeOrigin?: boolean
35
+ flipY?: boolean
36
+ scale?: number
37
+ applyBinding?: boolean
38
+ parseOptions?: any
39
+ /**
40
+ * 사용자 자유서술 — 도면 설명 / 강조 부분 / 분류 힌트 등.
41
+ * VLM 이 view type 분류 + entity 추출 양쪽 패스에 모두 추가 컨텍스트로 사용.
42
+ */
43
+ userPrompt?: string
44
+ }
45
+
46
+ // ── service 함수들 ────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * 비동기 import job 시작. ImportSession 즉시 반환, 백그라운드에서 worker 실행.
50
+ *
51
+ * resolver 와 LLM tool builder 양쪽이 공유하는 진입점. side-effect 인 worker 가동도
52
+ * 본 함수에 묶음 — 사용자 입장에선 "단일 호출 = import 시작" 의미.
53
+ *
54
+ * `startWorker` 는 의존성 주입 — default 는 setImmediate(runImportJob). 단위 테스트는
55
+ * noop 주입해 worker chain 이 jest 환경에서 평가되지 않도록 한다 (typeorm DataSource init
56
+ * 안 된 환경에서 entity decorator 평가 시 fail 회피).
57
+ */
58
+ export async function startImportJobAsync(
59
+ input: StartImportJobInput,
60
+ context: ImportActionsContext,
61
+ sessionRepo: SessionRepoLike,
62
+ startWorker: (sessionId: string) => void = defaultStartWorker
63
+ ): Promise<ImportSession> {
64
+ const session = await sessionRepo.save({
65
+ domain: context.domain,
66
+ attachmentId: input.attachmentId,
67
+ chatSessionId: input.chatSessionId,
68
+ status: 'queued',
69
+ progress: 0,
70
+ message: 'Queued',
71
+ optionsJson: JSON.stringify({
72
+ scopes: input.scopes,
73
+ normalizeOrigin: input.normalizeOrigin,
74
+ flipY: input.flipY,
75
+ scale: input.scale,
76
+ applyBinding: input.applyBinding,
77
+ parseOptions: input.parseOptions,
78
+ userPrompt: input.userPrompt
79
+ }),
80
+ creator: context.user
81
+ } as any)
82
+
83
+ // 백그라운드 worker 시작 — 의존성 주입으로 분리 (테스트 격리 + lazy require).
84
+ startWorker(session.id!)
85
+ return session
86
+ }
87
+
88
+ /**
89
+ * Default startWorker — setImmediate(runImportJob). worker module 은 lazy require 로
90
+ * module-level 평가 chain (attachment-base STORAGE, pipeline 의 adapters 등) 을 회피.
91
+ */
92
+ function defaultStartWorker(sessionId: string): void {
93
+ setImmediate(() => {
94
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
95
+ const { runImportJob } = require('./import-worker')
96
+ runImportJob(sessionId).catch((err: Error) => {
97
+ console.error('[board-import] runImportJob unhandled:', err)
98
+ })
99
+ })
100
+ }
101
+
102
+ /**
103
+ * 단일 ImportSession 조회 (도메인 격리). 미존재면 null.
104
+ */
105
+ export async function fetchImportSession(
106
+ sessionId: string,
107
+ context: ImportActionsContext,
108
+ sessionRepo: SessionRepoLike
109
+ ): Promise<ImportSession | null> {
110
+ return await sessionRepo.findOneBy({
111
+ id: sessionId,
112
+ domain: { id: context.domain.id }
113
+ })
114
+ }
115
+
116
+ /**
117
+ * 한 ChatSession 안에서 발동된 ImportSession 목록 (최신순 50개). board-ai 의 chat 흐름이
118
+ * "이 chat 의 import 들 보여줘" 같은 호출에서 사용.
119
+ */
120
+ export async function listImportSessionsByChatSession(
121
+ chatSessionId: string,
122
+ context: ImportActionsContext,
123
+ sessionRepo: SessionRepoLike
124
+ ): Promise<ImportSession[]> {
125
+ return await sessionRepo.find({
126
+ where: { chatSessionId, domain: { id: context.domain.id } },
127
+ order: { createdAt: 'DESC' },
128
+ take: 50
129
+ })
130
+ }
131
+
132
+ /**
133
+ * 등록된 FormatAdapter 들의 능력 정보. context 무관 (인스턴스만 inspect).
134
+ * 외부 RPC 의존성도 없어 LLM 이 안전하게 호출 가능한 read tool.
135
+ */
136
+ export interface AdapterInfo {
137
+ format: string
138
+ supportedExtensions: string[]
139
+ capabilities: {
140
+ has2D: boolean
141
+ has3D: boolean
142
+ hasSemanticLabels: boolean
143
+ hasTextLabels: boolean
144
+ hasAttributes: boolean
145
+ }
146
+ }
147
+
148
+ export function describeAdapter(adapter: {
149
+ format: string
150
+ supportedExtensions: string[]
151
+ capabilities: any
152
+ }): AdapterInfo {
153
+ return {
154
+ format: adapter.format,
155
+ supportedExtensions: [...adapter.supportedExtensions],
156
+ capabilities: {
157
+ has2D: !!adapter.capabilities.has2D,
158
+ has3D: !!adapter.capabilities.has3D,
159
+ hasSemanticLabels: !!adapter.capabilities.hasSemanticLabels,
160
+ hasTextLabels: !!adapter.capabilities.hasTextLabels,
161
+ hasAttributes: !!adapter.capabilities.hasAttributes
162
+ }
163
+ }
164
+ }