@things-factory/integration-label-studio 9.1.19

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 (152) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/EXTERNAL_DATA_SOURCING.md +484 -0
  3. package/IMPLEMENTATION_GUIDE.md +469 -0
  4. package/INTEGRATION.md +279 -0
  5. package/README.md +1014 -0
  6. package/SETUP_GUIDE.md +577 -0
  7. package/TEST_GUIDE.md +387 -0
  8. package/UI_CUSTOMIZATION.md +395 -0
  9. package/USER_SYNC_GUIDE.md +514 -0
  10. package/client/bootstrap.ts +1 -0
  11. package/client/index.ts +1 -0
  12. package/client/label-studio-label-page.ts +52 -0
  13. package/client/label-studio-project-create.ts +216 -0
  14. package/client/label-studio-project-list.ts +214 -0
  15. package/client/label-studio-wrapper.ts +294 -0
  16. package/client/route.ts +15 -0
  17. package/client/tsconfig.json +13 -0
  18. package/config/config.development.js +124 -0
  19. package/config/config.production.js +182 -0
  20. package/dist-client/bootstrap.d.ts +1 -0
  21. package/dist-client/bootstrap.js +2 -0
  22. package/dist-client/bootstrap.js.map +1 -0
  23. package/dist-client/index.d.ts +1 -0
  24. package/dist-client/index.js +2 -0
  25. package/dist-client/index.js.map +1 -0
  26. package/dist-client/label-studio-label-page.d.ts +8 -0
  27. package/dist-client/label-studio-label-page.js +54 -0
  28. package/dist-client/label-studio-label-page.js.map +1 -0
  29. package/dist-client/label-studio-project-create.d.ts +16 -0
  30. package/dist-client/label-studio-project-create.js +235 -0
  31. package/dist-client/label-studio-project-create.js.map +1 -0
  32. package/dist-client/label-studio-project-list.d.ts +16 -0
  33. package/dist-client/label-studio-project-list.js +222 -0
  34. package/dist-client/label-studio-project-list.js.map +1 -0
  35. package/dist-client/label-studio-wrapper.d.ts +57 -0
  36. package/dist-client/label-studio-wrapper.js +304 -0
  37. package/dist-client/label-studio-wrapper.js.map +1 -0
  38. package/dist-client/route.d.ts +1 -0
  39. package/dist-client/route.js +14 -0
  40. package/dist-client/route.js.map +1 -0
  41. package/dist-client/tsconfig.tsbuildinfo +1 -0
  42. package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
  43. package/dist-server/controller/label-studio-role-mapper.js +65 -0
  44. package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
  45. package/dist-server/controller/user-provisioning-service.d.ts +66 -0
  46. package/dist-server/controller/user-provisioning-service.js +264 -0
  47. package/dist-server/controller/user-provisioning-service.js.map +1 -0
  48. package/dist-server/index.d.ts +7 -0
  49. package/dist-server/index.js +19 -0
  50. package/dist-server/index.js.map +1 -0
  51. package/dist-server/route/label-studio-sso.d.ts +2 -0
  52. package/dist-server/route/label-studio-sso.js +156 -0
  53. package/dist-server/route/label-studio-sso.js.map +1 -0
  54. package/dist-server/route/webhook.d.ts +65 -0
  55. package/dist-server/route/webhook.js +248 -0
  56. package/dist-server/route/webhook.js.map +1 -0
  57. package/dist-server/route.d.ts +1 -0
  58. package/dist-server/route.js +21 -0
  59. package/dist-server/route.js.map +1 -0
  60. package/dist-server/service/ai-prediction-service.d.ts +27 -0
  61. package/dist-server/service/ai-prediction-service.js +222 -0
  62. package/dist-server/service/ai-prediction-service.js.map +1 -0
  63. package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
  64. package/dist-server/service/dataset-labeling-integration.js +512 -0
  65. package/dist-server/service/dataset-labeling-integration.js.map +1 -0
  66. package/dist-server/service/external-data-source-service.d.ts +78 -0
  67. package/dist-server/service/external-data-source-service.js +415 -0
  68. package/dist-server/service/external-data-source-service.js.map +1 -0
  69. package/dist-server/service/index.d.ts +12 -0
  70. package/dist-server/service/index.js +27 -0
  71. package/dist-server/service/index.js.map +1 -0
  72. package/dist-server/service/label-studio-sso-service.d.ts +38 -0
  73. package/dist-server/service/label-studio-sso-service.js +98 -0
  74. package/dist-server/service/label-studio-sso-service.js.map +1 -0
  75. package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
  76. package/dist-server/service/ml/ml-backend-service.js +153 -0
  77. package/dist-server/service/ml/ml-backend-service.js.map +1 -0
  78. package/dist-server/service/prediction/prediction-management.d.ts +32 -0
  79. package/dist-server/service/prediction/prediction-management.js +299 -0
  80. package/dist-server/service/prediction/prediction-management.js.map +1 -0
  81. package/dist-server/service/project/project-management.d.ts +36 -0
  82. package/dist-server/service/project/project-management.js +309 -0
  83. package/dist-server/service/project/project-management.js.map +1 -0
  84. package/dist-server/service/task/task-management.d.ts +42 -0
  85. package/dist-server/service/task/task-management.js +372 -0
  86. package/dist-server/service/task/task-management.js.map +1 -0
  87. package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
  88. package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
  89. package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
  90. package/dist-server/service/webhook/webhook-management.d.ts +21 -0
  91. package/dist-server/service/webhook/webhook-management.js +134 -0
  92. package/dist-server/service/webhook/webhook-management.js.map +1 -0
  93. package/dist-server/tsconfig.tsbuildinfo +1 -0
  94. package/dist-server/types/dataset-labeling-types.d.ts +71 -0
  95. package/dist-server/types/dataset-labeling-types.js +259 -0
  96. package/dist-server/types/dataset-labeling-types.js.map +1 -0
  97. package/dist-server/types/label-studio-types.d.ts +128 -0
  98. package/dist-server/types/label-studio-types.js +494 -0
  99. package/dist-server/types/label-studio-types.js.map +1 -0
  100. package/dist-server/types/prediction-types.d.ts +39 -0
  101. package/dist-server/types/prediction-types.js +121 -0
  102. package/dist-server/types/prediction-types.js.map +1 -0
  103. package/dist-server/utils/annotation-exporter.d.ts +104 -0
  104. package/dist-server/utils/annotation-exporter.js +261 -0
  105. package/dist-server/utils/annotation-exporter.js.map +1 -0
  106. package/dist-server/utils/label-config-builder.d.ts +117 -0
  107. package/dist-server/utils/label-config-builder.js +286 -0
  108. package/dist-server/utils/label-config-builder.js.map +1 -0
  109. package/dist-server/utils/label-studio-api-client.d.ts +180 -0
  110. package/dist-server/utils/label-studio-api-client.js +401 -0
  111. package/dist-server/utils/label-studio-api-client.js.map +1 -0
  112. package/dist-server/utils/media-url-extractor.d.ts +45 -0
  113. package/dist-server/utils/media-url-extractor.js +152 -0
  114. package/dist-server/utils/media-url-extractor.js.map +1 -0
  115. package/dist-server/utils/task-transformer.d.ts +108 -0
  116. package/dist-server/utils/task-transformer.js +260 -0
  117. package/dist-server/utils/task-transformer.js.map +1 -0
  118. package/package.json +47 -0
  119. package/server/SERVER_STRUCTURE.md +351 -0
  120. package/server/controller/label-studio-role-mapper.ts +76 -0
  121. package/server/controller/user-provisioning-service.ts +340 -0
  122. package/server/index.ts +19 -0
  123. package/server/route/label-studio-sso.ts +194 -0
  124. package/server/route/webhook.ts +304 -0
  125. package/server/route.ts +35 -0
  126. package/server/service/ai-prediction-service.ts +239 -0
  127. package/server/service/dataset-labeling-integration.ts +590 -0
  128. package/server/service/external-data-source-service.ts +438 -0
  129. package/server/service/index.ts +24 -0
  130. package/server/service/label-studio-sso-service.ts +108 -0
  131. package/server/service/labeling-scenario-service.ts.deprecated +566 -0
  132. package/server/service/ml/ml-backend-service.ts +127 -0
  133. package/server/service/prediction/prediction-management.ts +281 -0
  134. package/server/service/project/project-management.ts +284 -0
  135. package/server/service/task/task-management.ts +363 -0
  136. package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
  137. package/server/service/webhook/webhook-management.ts +109 -0
  138. package/server/tsconfig.json +11 -0
  139. package/server/types/dataset-labeling-types.ts +181 -0
  140. package/server/types/global.d.ts +23 -0
  141. package/server/types/label-studio-types.ts +346 -0
  142. package/server/types/prediction-types.ts +86 -0
  143. package/server/types/scenario-types.ts.deprecated +362 -0
  144. package/server/utils/annotation-exporter.ts +340 -0
  145. package/server/utils/label-config-builder.ts +340 -0
  146. package/server/utils/label-studio-api-client.ts +487 -0
  147. package/server/utils/media-url-extractor.ts +193 -0
  148. package/server/utils/task-transformer.ts +342 -0
  149. package/test-ai-prediction.js +268 -0
  150. package/test-dataset-integration.js +449 -0
  151. package/test-simple.js +89 -0
  152. package/things-factory.config.js +12 -0
package/README.md ADDED
@@ -0,0 +1,1014 @@
1
+ # @things-factory/integration-label-studio
2
+
3
+ Things-Factory와 Label Studio Custom을 완전 통합하는 엔터프라이즈급 모듈입니다.
4
+
5
+ [![Label Studio Version](https://img.shields.io/badge/Label%20Studio-1.20.0--sso.32-blue)](https://github.com/aidoop/label-studio-custom)
6
+ [![Docker Image](https://img.shields.io/badge/docker-ghcr.io%2Faidoop%2Flabel--studio--custom-blue)](https://github.com/aidoop/label-studio-custom/pkgs/container/label-studio-custom)
7
+
8
+ ## 지원 버전
9
+
10
+ - **Label Studio Custom**: `ghcr.io/aidoop/label-studio-custom:1.20.0-sso.38`
11
+ - **label-studio-sso**: `v6.0.8` (내장)
12
+ - **Things-Factory**: `9.1.0+`
13
+
14
+ ## 주요 기능
15
+
16
+ ### ✅ 이미 구현된 기능
17
+
18
+ - **iframe 통합** - Label Studio를 Things-Factory 화면에 임베드
19
+ - **SSO 인증** - JWT 토큰 기반 자동 로그인 (label-studio-sso v6.0.8 내장)
20
+ - **사용자 동기화** - Things-Factory ↔ Label Studio 사용자 배치 동기화
21
+ - **권한 매핑** - Things-Factory 권한 → Label Studio 역할 자동 매핑
22
+ - **자동 Organization 할당** - SSO 로그인 시 자동으로 Organization 멤버 추가
23
+
24
+ ### 🆕 새로 추가된 기능
25
+
26
+ - **프로젝트 관리** - 프로젝트 생성, 조회, 수정, 삭제 (CRUD)
27
+ - **태스크 관리** - 태스크 임포트, 익스포트, 어노테이션 조회
28
+ - **웹훅 연동** - Label Studio 이벤트 실시간 수신
29
+ - **ML Backend 통합** - 자동 예측 및 모델 학습 지원
30
+ - **통계 대시보드** - 프로젝트 진행률, 사용자 생산성 통계
31
+ - **UI 컴포넌트** - 프로젝트 목록, 생성 폼, 통계 위젯
32
+
33
+ ## 아키텍처
34
+
35
+ ### 전체 구조
36
+
37
+ ```
38
+ ┌─────────────────────────────────────────────────────────┐
39
+ │ Things-Factory │
40
+ │ │
41
+ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
42
+ │ │ GraphQL │ │ Webhook │ │ UI Components │ │
43
+ │ │ API │ │ Handler │ │ │ │
44
+ │ └──────┬──────┘ └──────┬───────┘ └───────┬───────┘ │
45
+ │ │ │ │ │
46
+ │ └────────────────┴──────────────────┘ │
47
+ │ │ │
48
+ │ ┌───────▼────────┐ │
49
+ │ │ API Client │ │
50
+ │ └───────┬────────┘ │
51
+ └──────────────────────────┼──────────────────────────────┘
52
+
53
+ ┌────────▼─────────┐
54
+ │ Label Studio │
55
+ │ REST API │
56
+ └──────────────────┘
57
+ ```
58
+
59
+ ### 유연한 확장 아키텍처
60
+
61
+ 이 모듈은 **플러그인 기반의 유연한 아키텍처**를 제공합니다:
62
+
63
+ ```
64
+ ┌─────────────────────────────────────────────────────────┐
65
+ │ Application Layer │
66
+ │ │
67
+ │ • registerWebhookHandler() - 웹훅 핸들러 등록 │
68
+ │ • registerExportFormat() - 익스포트 포맷 등록 │
69
+ │ • LabelConfigBuilder - 프로젝트 설정 생성 │
70
+ │ • TaskTransformer - 데이터 변환 │
71
+ └─────────────────────────────────────────────────────────┘
72
+
73
+ ┌──────────────────────────▼──────────────────────────────┐
74
+ │ integration-label-studio (Base Layer) │
75
+ │ │
76
+ │ • LabelConfigBuilder - 선언적 설정 생성 │
77
+ │ • TaskTransformer - 유연한 데이터 변환 │
78
+ │ • AnnotationExporter - 플러그인형 익스포트 │
79
+ │ • Webhook Extension - 확장 가능한 웹훅 │
80
+ │ • GraphQL API - 유연한 입력 지원 │
81
+ └─────────────────────────────────────────────────────────┘
82
+ ```
83
+
84
+ ## 빠른 시작
85
+
86
+ ### 1. Label Studio Custom Docker 실행
87
+
88
+ ```bash
89
+ # Docker Compose로 Label Studio Custom 실행
90
+ cd /path/to/label-studio-custom
91
+
92
+ # .env 파일 설정
93
+ cat > .env << 'EOF'
94
+ POSTGRES_DB=labelstudio
95
+ POSTGRES_USER=postgres
96
+ POSTGRES_PASSWORD=dev_postgres_2024
97
+
98
+ # Label Studio 접속 URL
99
+ LABEL_STUDIO_HOST=http://label.hatiolab.localhost:8080
100
+
101
+ # 서브도메인 간 쿠키 공유 (SSO 필수)
102
+ COOKIE_DOMAIN=.hatiolab.localhost
103
+ SESSION_COOKIE_DOMAIN=.hatiolab.localhost
104
+ CSRF_COOKIE_DOMAIN=.hatiolab.localhost
105
+
106
+ # iframe 임베딩 보안 헤더
107
+ CSP_FRAME_ANCESTORS='self' http://localhost:3000 http://hatiolab.localhost:3000
108
+
109
+ # API 토큰 (초기 사용자 생성 후 설정)
110
+ LABEL_STUDIO_API_TOKEN=your-api-token-here
111
+ EOF
112
+
113
+ # Docker Compose 실행
114
+ docker compose up -d
115
+
116
+ # 로그 확인
117
+ docker compose logs -f labelstudio
118
+
119
+ # 초기 Admin 사용자 생성 (최초 1회)
120
+ docker compose exec labelstudio python manage.py createsuperuser
121
+
122
+ # API 토큰 발급
123
+ # → http://label.hatiolab.localhost:8080 로그인
124
+ # → Account Settings → Access Token → Create new token
125
+ # → 생성된 토큰을 .env 파일의 LABEL_STUDIO_API_TOKEN에 설정
126
+ ```
127
+
128
+ ### 2. Things-Factory 설정
129
+
130
+ **packages/integration-label-studio/config/label-studio.config.js:**
131
+
132
+ ```javascript
133
+ module.exports = {
134
+ labelStudio: {
135
+ serverUrl: process.env.LABEL_STUDIO_URL || 'http://label.hatiolab.localhost:8080',
136
+ apiToken: process.env.LABEL_STUDIO_API_TOKEN || '',
137
+ cookieDomain: process.env.LABEL_STUDIO_COOKIE_DOMAIN || '.hatiolab.localhost',
138
+ interfaces: 'panel,controls,annotations:menu'
139
+ }
140
+ }
141
+ ```
142
+
143
+ **Things-Factory .env (Development):**
144
+
145
+ ```bash
146
+ LABEL_STUDIO_URL=http://label.hatiolab.localhost:8080
147
+ LABEL_STUDIO_API_TOKEN=your-api-token-here
148
+ LABEL_STUDIO_COOKIE_DOMAIN=.hatiolab.localhost
149
+ ```
150
+
151
+ **Things-Factory .env (Production):**
152
+
153
+ ```bash
154
+ LABEL_STUDIO_URL=https://label.example.com
155
+ LABEL_STUDIO_API_TOKEN=your-api-token-here
156
+ LABEL_STUDIO_COOKIE_DOMAIN=.example.com
157
+ ```
158
+
159
+ ### 3. Things-Factory 빌드 및 실행
160
+
161
+ ```bash
162
+ cd /path/to/things-factory
163
+
164
+ # integration-label-studio 모듈 빌드
165
+ cd packages/integration-label-studio
166
+ yarn build
167
+
168
+ # 루트로 돌아가서 실행
169
+ cd ../..
170
+ yarn workspace @things-factory/operato-mms run serve:dev
171
+ ```
172
+
173
+ ### 4. 접속
174
+
175
+ - **Things Factory**: http://hatiolab.localhost:3000 → 메뉴 → Label Studio
176
+ - **Label Studio**: http://label.hatiolab.localhost:8080 (직접 접속)
177
+ - **GraphQL Playground**: http://hatiolab.localhost:3000/graphql
178
+
179
+ **참고**: `*.localhost` 도메인은 브라우저가 자동으로 `127.0.0.1`로 해석하므로 `/etc/hosts` 수정이 필요 없습니다.
180
+
181
+ ## 유연한 확장 기능
182
+
183
+ ### 1. LabelConfigBuilder - 선언적 프로젝트 설정
184
+
185
+ XML을 직접 작성하지 않고, **선언적 사양**으로 Label Studio 설정을 생성:
186
+
187
+ ```typescript
188
+ import { LabelConfigBuilder, LabelConfigTemplates } from '@things-factory/integration-label-studio'
189
+
190
+ // 방법 1: 템플릿 사용
191
+ const config = LabelConfigTemplates.imageRankN(3, ['Normal', 'Defect A', 'Defect B', 'Defect C'])
192
+ const xml = LabelConfigBuilder.build(config)
193
+
194
+ // 방법 2: 커스텀 설정
195
+ const customConfig = LabelConfigBuilder.build({
196
+ dataType: 'image',
197
+ controls: [
198
+ {
199
+ type: 'choices',
200
+ name: 'rank1',
201
+ toName: 'data',
202
+ config: { choices: ['Normal', 'Defect'], choice: 'single', required: true }
203
+ },
204
+ {
205
+ type: 'rating',
206
+ name: 'confidence',
207
+ toName: 'data',
208
+ config: { maxRating: 5, icon: 'star' }
209
+ }
210
+ ]
211
+ })
212
+ ```
213
+
214
+ ### 2. TaskTransformer - 유연한 데이터 변환
215
+
216
+ 임의의 소스 데이터를 Label Studio 태스크로 변환:
217
+
218
+ ```typescript
219
+ import { TaskTransformer, TaskTransformTemplates } from '@things-factory/integration-label-studio'
220
+
221
+ // WBM 디바이스 데이터 → Label Studio 태스크
222
+ const sourceData = [
223
+ {
224
+ image_url: 'http://device/image1.jpg',
225
+ device_id: 'WBM-001',
226
+ wafer_id: 'W123',
227
+ timestamp: '2025-01-01T10:00:00Z',
228
+ ai_prediction: { rank1: 'Normal', rank2: 'Defect A', rank3: 'Defect B' },
229
+ confidence: 0.95
230
+ }
231
+ ]
232
+
233
+ // 변환 규칙
234
+ const rule = TaskTransformTemplates.wbmImageClassification(true)
235
+
236
+ // 변환 실행
237
+ const lsTasks = TaskTransformer.transform(sourceData, rule)
238
+ // 결과: Label Studio 태스크 (data, predictions, meta)
239
+ ```
240
+
241
+ **커스텀 변환 규칙:**
242
+
243
+ ```typescript
244
+ const customRule = {
245
+ dataFields: {
246
+ image: 'images[0].url', // 중첩 배열 지원
247
+ date: 'metadata.captured_at', // 중첩 객체 지원
248
+ device: 'device.id'
249
+ },
250
+ predictions: {
251
+ enabled: true,
252
+ resultPath: 'ai.classification',
253
+ scorePath: 'ai.confidence',
254
+ modelVersion: 'v1.0.0',
255
+ resultTransform: result => {
256
+ // AI 결과를 Label Studio 포맷으로 변환
257
+ return [{ from_name: 'label', to_name: 'data', type: 'choices', value: { choices: [result] } }]
258
+ }
259
+ },
260
+ meta: {
261
+ lot: 'production.lot_id',
262
+ line: 'production.line_number'
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### 3. Webhook Handler 확장
268
+
269
+ 웹훅 이벤트에 커스텀 로직 추가:
270
+
271
+ ```typescript
272
+ import { registerWebhookHandler, WebhookAction } from '@things-factory/integration-label-studio'
273
+
274
+ // 어노테이션 완료 시 자동 처리
275
+ registerWebhookHandler(WebhookAction.ANNOTATION_CREATED, async (payload, ctx) => {
276
+ console.log('New annotation:', payload.annotation.id)
277
+
278
+ // 1. Things-Factory DB에 저장
279
+ await saveAnnotationToDatabase(payload.annotation)
280
+
281
+ // 2. 품질 시스템에 전송
282
+ await sendToQualitySystem(payload.annotation)
283
+
284
+ // 3. 임계값 초과 시 알림
285
+ if (payload.annotation.result.includes('Defect')) {
286
+ await sendAlertToManager(payload.project.id)
287
+ }
288
+ })
289
+
290
+ // 여러 핸들러 등록 가능 (순차 실행)
291
+ registerWebhookHandler(WebhookAction.ANNOTATION_CREATED, async (payload, ctx) => {
292
+ // 통계 업데이트
293
+ await updateProjectStats(payload.project.id)
294
+ })
295
+ ```
296
+
297
+ ### 4. AnnotationExporter - 플러그인형 익스포트
298
+
299
+ 커스텀 익스포트 포맷 추가:
300
+
301
+ ```typescript
302
+ import { registerExportFormat, exportAnnotations } from '@things-factory/integration-label-studio'
303
+
304
+ // 커스텀 포맷 등록
305
+ registerExportFormat('wbm-report', (annotations, context) => {
306
+ const report = {
307
+ project: context.projectTitle,
308
+ exported_at: context.exportedAt,
309
+ summary: {
310
+ total: annotations.length,
311
+ defect_count: annotations.filter(a => a.result.includes('Defect')).length
312
+ },
313
+ details: annotations.map(a => ({
314
+ task_id: a.task,
315
+ wafer_id: a.meta?.wafer_id,
316
+ classification: a.result[0].value.choices[0],
317
+ annotator: a.completed_by.email,
318
+ time: a.lead_time
319
+ }))
320
+ }
321
+
322
+ return JSON.stringify(report, null, 2)
323
+ })
324
+
325
+ // 사용
326
+ const data = await exportAnnotations(annotations, 'wbm-report', { projectId: 1, exportedAt: new Date().toISOString() })
327
+ ```
328
+
329
+ **기본 제공 포맷:**
330
+
331
+ - `json` - Label Studio 기본 JSON
332
+ - `jsonl` - JSON Lines (한 줄에 하나씩)
333
+ - `csv` - 이미지 분류 CSV
334
+ - `rank-csv` - Rank N 분류 CSV
335
+ - `coco` - COCO object detection
336
+ - `yolo` - YOLO object detection
337
+ - `ner-json` - Named Entity Recognition
338
+
339
+ ## GraphQL API
340
+
341
+ ### 기본 프로젝트 관리
342
+
343
+ ```graphql
344
+ # 프로젝트 목록 조회
345
+ query {
346
+ labelStudioProjects {
347
+ id
348
+ title
349
+ description
350
+ taskCount
351
+ completedTaskCount
352
+ completionRate
353
+ }
354
+ }
355
+
356
+ # 프로젝트 생성 (XML 직접 작성)
357
+ mutation {
358
+ createLabelStudioProject(
359
+ input: {
360
+ title: "Image Classification"
361
+ description: "Classify product images"
362
+ labelConfig: """
363
+ <View>
364
+ <Image name="image" value="$image"/>
365
+ <Choices name="category" toName="image">
366
+ <Choice value="Electronics"/>
367
+ <Choice value="Clothing"/>
368
+ <Choice value="Food"/>
369
+ </Choices>
370
+ </View>
371
+ """
372
+ }
373
+ ) {
374
+ id
375
+ title
376
+ }
377
+ }
378
+
379
+ # 🆕 프로젝트 생성 (선언적 사양 사용)
380
+ mutation {
381
+ createLabelStudioProjectWithSpec(
382
+ input: {
383
+ title: "WBM Rank 3 Classification"
384
+ description: "Wafer defect classification"
385
+ labelConfigSpec: {
386
+ dataType: "image"
387
+ controls: """
388
+ [
389
+ {
390
+ "type": "choices",
391
+ "name": "rank1",
392
+ "toName": "data",
393
+ "config": {
394
+ "choices": ["Normal", "Defect A", "Defect B", "Defect C"],
395
+ "choice": "single",
396
+ "required": true
397
+ }
398
+ },
399
+ {
400
+ "type": "choices",
401
+ "name": "rank2",
402
+ "toName": "data",
403
+ "config": {
404
+ "choices": ["Normal", "Defect A", "Defect B", "Defect C"],
405
+ "choice": "single"
406
+ }
407
+ },
408
+ {
409
+ "type": "choices",
410
+ "name": "rank3",
411
+ "toName": "data",
412
+ "config": {
413
+ "choices": ["Normal", "Defect A", "Defect B", "Defect C"],
414
+ "choice": "single"
415
+ }
416
+ }
417
+ ]
418
+ """
419
+ }
420
+ }
421
+ ) {
422
+ id
423
+ labelConfig
424
+ }
425
+ }
426
+
427
+ # 프로젝트 통계
428
+ query {
429
+ labelStudioProjectMetrics(projectId: 1) {
430
+ totalTasks
431
+ completedTasks
432
+ completionRate
433
+ avgTimePerTask
434
+ annotatorStats {
435
+ email
436
+ annotationCount
437
+ avgTime
438
+ }
439
+ }
440
+ }
441
+
442
+ # 프로젝트 삭제
443
+ mutation {
444
+ deleteLabelStudioProject(projectId: 1)
445
+ }
446
+ ```
447
+
448
+ ### 태스크 관리
449
+
450
+ ```graphql
451
+ # 태스크 조회
452
+ query {
453
+ labelStudioTasks(projectId: 1, page: 1, pageSize: 100) {
454
+ id
455
+ data
456
+ annotationCount
457
+ isCompleted
458
+ }
459
+ }
460
+
461
+ # 태스크 임포트 (기본)
462
+ mutation {
463
+ importTasksToLabelStudio(
464
+ projectId: 1
465
+ tasks: [
466
+ { data: "{\"image\": \"https://example.com/image1.jpg\"}" }
467
+ { data: "{\"image\": \"https://example.com/image2.jpg\"}" }
468
+ ]
469
+ ) {
470
+ imported
471
+ failed
472
+ taskIds
473
+ errors
474
+ }
475
+ }
476
+
477
+ # 🆕 태스크 임포트 (데이터 변환 사용)
478
+ mutation {
479
+ importTasksWithTransform(
480
+ projectId: 1
481
+ input: {
482
+ sourceData: """
483
+ [
484
+ {
485
+ "image_url": "http://device/img1.jpg",
486
+ "device_id": "WBM-001",
487
+ "wafer_id": "W123",
488
+ "lot_id": "LOT-001",
489
+ "timestamp": "2025-01-01T10:00:00Z",
490
+ "ai_prediction": {
491
+ "rank1": "Normal",
492
+ "rank2": "Defect A",
493
+ "rank3": "Defect B"
494
+ },
495
+ "confidence": 0.95
496
+ }
497
+ ]
498
+ """
499
+ transformRule: {
500
+ dataFields: """
501
+ {
502
+ "image": "image_url",
503
+ "date": "timestamp",
504
+ "device_id": "device_id"
505
+ }
506
+ """
507
+ predictions: """
508
+ {
509
+ "enabled": true,
510
+ "resultPath": "ai_prediction",
511
+ "scorePath": "confidence",
512
+ "modelVersion": "v1.0"
513
+ }
514
+ """
515
+ meta: """
516
+ {
517
+ "wafer_id": "wafer_id",
518
+ "lot_id": "lot_id"
519
+ }
520
+ """
521
+ }
522
+ }
523
+ ) {
524
+ imported
525
+ taskIds
526
+ }
527
+ }
528
+
529
+ # 어노테이션 조회
530
+ query {
531
+ labelStudioTaskAnnotations(taskId: 1) {
532
+ id
533
+ result
534
+ completedBy
535
+ leadTime
536
+ createdAt
537
+ }
538
+ }
539
+
540
+ # 어노테이션 익스포트 (기본)
541
+ mutation {
542
+ exportLabelStudioAnnotations(
543
+ projectId: 1
544
+ format: "JSON" # JSON, CSV, COCO, YOLO 등
545
+ ) {
546
+ exportPath
547
+ annotationCount
548
+ format
549
+ }
550
+ }
551
+
552
+ # 🆕 어노테이션 익스포트 (커스텀 포맷)
553
+ mutation {
554
+ exportAnnotationsWithFormat(
555
+ projectId: 1
556
+ input: {
557
+ format: "rank-csv" # json, jsonl, csv, rank-csv, coco, yolo, ner-json, 또는 커스텀
558
+ taskIds: "[1, 2, 3]" # 선택적 필터
559
+ }
560
+ ) {
561
+ data
562
+ count
563
+ format
564
+ }
565
+ }
566
+
567
+ # Things-Factory DB에 동기화
568
+ mutation {
569
+ syncAnnotationsToDatabase(projectId: 1)
570
+ }
571
+ ```
572
+
573
+ ### 웹훅 관리
574
+
575
+ ```graphql
576
+ # 웹훅 등록 (실시간 이벤트 수신)
577
+ mutation {
578
+ registerLabelStudioWebhook(projectId: 1) {
579
+ id
580
+ url
581
+ isActive
582
+ }
583
+ }
584
+
585
+ # 웹훅 조회
586
+ query {
587
+ labelStudioWebhooks(projectId: 1) {
588
+ id
589
+ url
590
+ isActive
591
+ }
592
+ }
593
+
594
+ # 웹훅 삭제
595
+ mutation {
596
+ deleteLabelStudioWebhook(webhookId: 1)
597
+ }
598
+ ```
599
+
600
+ ### ML Backend 연동
601
+
602
+ ```graphql
603
+ # ML Backend 조회
604
+ query {
605
+ labelStudioMLBackends(projectId: 1) {
606
+ id
607
+ url
608
+ title
609
+ isInteractive
610
+ modelVersion
611
+ }
612
+ }
613
+
614
+ # ML Backend 추가
615
+ mutation {
616
+ addMLBackendToProject(
617
+ projectId: 1
618
+ input: { url: "http://ml-backend:9090", title: "Auto-labeling Model", isInteractive: true }
619
+ ) {
620
+ id
621
+ url
622
+ }
623
+ }
624
+
625
+ # 예측 실행
626
+ mutation {
627
+ triggerLabelStudioPredictions(
628
+ projectId: 1
629
+ taskIds: [1, 2, 3] # null이면 전체
630
+ )
631
+ }
632
+
633
+ # 모델 학습
634
+ mutation {
635
+ trainLabelStudioModel(mlBackendId: 1)
636
+ }
637
+ ```
638
+
639
+ ### 사용자 동기화
640
+
641
+ ```graphql
642
+ # 내 계정 동기화
643
+ mutation {
644
+ syncMyUserToLabelStudio {
645
+ success
646
+ email
647
+ action
648
+ lsUserId
649
+ lsPermissions
650
+ }
651
+ }
652
+
653
+ # 전체 사용자 동기화 (관리자만)
654
+ mutation {
655
+ syncAllUsersToLabelStudio {
656
+ total
657
+ created
658
+ updated
659
+ deactivated
660
+ skipped
661
+ errors
662
+ results {
663
+ email
664
+ action
665
+ lsPermissions
666
+ }
667
+ }
668
+ }
669
+ ```
670
+
671
+ ## UI 컴포넌트
672
+
673
+ ### 프로젝트 목록
674
+
675
+ ```
676
+ 메뉴 → Label Studio → Projects
677
+ ```
678
+
679
+ - 프로젝트 카드 그리드 뷰
680
+ - 완료율 프로그레스 바
681
+ - 통계 표시 (태스크 수, 완료 수)
682
+ - 프로젝트 클릭 → Label Studio 오픈
683
+
684
+ ### 프로젝트 생성
685
+
686
+ ```
687
+ 메뉴 → Label Studio → Projects → New Project
688
+ ```
689
+
690
+ - 템플릿 지원 (Text Classification, Image Classification, NER)
691
+ - Label Config XML 편집기
692
+ - 실시간 검증
693
+
694
+ ### Label Studio Viewer
695
+
696
+ ```
697
+ 프로젝트 카드 클릭 → iframe으로 Label Studio 오픈
698
+ ```
699
+
700
+ - SSO 자동 로그인
701
+ - 최소화 UI (panel, controls, annotations:menu)
702
+
703
+ ## 웹훅 이벤트
704
+
705
+ Things-Factory가 수신하는 Label Studio 이벤트:
706
+
707
+ | 이벤트 | 설명 | 핸들러 |
708
+ | -------------------- | ----------------- | --------------------------- |
709
+ | `ANNOTATION_CREATED` | 어노테이션 생성 | `handleAnnotationCreated()` |
710
+ | `ANNOTATION_UPDATED` | 어노테이션 수정 | `handleAnnotationUpdated()` |
711
+ | `ANNOTATION_DELETED` | 어노테이션 삭제 | `handleAnnotationDeleted()` |
712
+ | `TASK_CREATED` | 태스크 생성 | `handleTaskCreated()` |
713
+ | `PROJECT_UPDATED` | 프로젝트 업데이트 | `handleProjectUpdated()` |
714
+
715
+ **웹훅 엔드포인트:**
716
+
717
+ ```
718
+ POST https://your-things-factory.com/label-studio/webhook
719
+ ```
720
+
721
+ **커스터마이징:**
722
+ `server/route/webhook.ts`의 핸들러 함수를 수정하여 비즈니스 로직 구현
723
+
724
+ ## 권한 매핑
725
+
726
+ | Things-Factory | Label Studio |
727
+ | ---------------------- | -------------------------- |
728
+ | Owner + label-studio | Admin (is_superuser=true) |
729
+ | label-studio privilege | Staff (is_superuser=false) |
730
+ | No label-studio | Inactive (is_active=false) |
731
+
732
+ ## 사용 예제
733
+
734
+ ### 1. 이미지 분류 프로젝트
735
+
736
+ ```graphql
737
+ # 1. 프로젝트 생성
738
+ mutation {
739
+ createLabelStudioProject(
740
+ input: {
741
+ title: "Product Image Classification"
742
+ labelConfig: """
743
+ <View>
744
+ <Image name="image" value="$image"/>
745
+ <Choices name="category" toName="image" choice="single">
746
+ <Choice value="Electronics"/>
747
+ <Choice value="Clothing"/>
748
+ <Choice value="Food"/>
749
+ </Choices>
750
+ </View>
751
+ """
752
+ }
753
+ ) {
754
+ id
755
+ }
756
+ }
757
+
758
+ # 2. 이미지 임포트
759
+ mutation {
760
+ importTasksToLabelStudio(projectId: 1, tasks: [{ data: "{\"image\": \"https://cdn.example.com/product1.jpg\"}" }]) {
761
+ imported
762
+ }
763
+ }
764
+
765
+ # 3. 웹훅 등록
766
+ mutation {
767
+ registerLabelStudioWebhook(projectId: 1) {
768
+ id
769
+ }
770
+ }
771
+
772
+ # 4. 완료 후 익스포트
773
+ mutation {
774
+ exportLabelStudioAnnotations(projectId: 1, format: "JSON") {
775
+ exportPath
776
+ }
777
+ }
778
+ ```
779
+
780
+ ### 2. 텍스트 감성 분석
781
+
782
+ ```graphql
783
+ mutation {
784
+ createLabelStudioProject(
785
+ input: {
786
+ title: "Customer Review Sentiment"
787
+ labelConfig: """
788
+ <View>
789
+ <Text name="text" value="$text"/>
790
+ <Choices name="sentiment" toName="text" choice="single">
791
+ <Choice value="Positive"/>
792
+ <Choice value="Negative"/>
793
+ <Choice value="Neutral"/>
794
+ </Choices>
795
+ </View>
796
+ """
797
+ }
798
+ ) {
799
+ id
800
+ }
801
+ }
802
+ ```
803
+
804
+ ### 3. Named Entity Recognition
805
+
806
+ ```graphql
807
+ mutation {
808
+ createLabelStudioProject(
809
+ input: {
810
+ title: "Document NER"
811
+ labelConfig: """
812
+ <View>
813
+ <Text name="text" value="$text"/>
814
+ <Labels name="label" toName="text">
815
+ <Label value="Person" background="red"/>
816
+ <Label value="Organization" background="blue"/>
817
+ <Label value="Location" background="green"/>
818
+ </Labels>
819
+ </View>
820
+ """
821
+ }
822
+ ) {
823
+ id
824
+ }
825
+ }
826
+ ```
827
+
828
+ ## 파일 구조
829
+
830
+ ```
831
+ integration-label-studio/
832
+ ├── server/
833
+ │ ├── types/
834
+ │ │ ├── label-studio-types.ts # GraphQL 타입 정의
835
+ │ │ └── global.d.ts # 전역 타입
836
+ │ ├── utils/
837
+ │ │ ├── label-studio-api-client.ts # REST API 클라이언트
838
+ │ │ ├── label-config-builder.ts # 🆕 선언적 설정 생성
839
+ │ │ ├── task-transformer.ts # 🆕 유연한 데이터 변환
840
+ │ │ └── annotation-exporter.ts # 🆕 플러그인형 익스포트
841
+ │ ├── service/
842
+ │ │ ├── project/
843
+ │ │ │ └── project-management.ts # 프로젝트 CRUD + 유연한 생성
844
+ │ │ ├── task/
845
+ │ │ │ └── task-management.ts # 태스크 관리 + 변환 임포트
846
+ │ │ ├── webhook/
847
+ │ │ │ └── webhook-management.ts # 웹훅 관리
848
+ │ │ ├── ml/
849
+ │ │ │ └── ml-backend-service.ts # ML Backend
850
+ │ │ └── user-provisioning/
851
+ │ │ └── user-sync-mutation.ts # 사용자 동기화
852
+ │ ├── route/
853
+ │ │ └── webhook.ts # 웹훅 라우터 + 확장 포인트
854
+ │ ├── route.ts # 메뉴 등록
855
+ │ └── index.ts # 모든 유틸리티 export
856
+ ├── client/
857
+ │ ├── label-studio-wrapper.ts # iframe 래퍼
858
+ │ ├── label-studio-project-list.ts # 프로젝트 목록
859
+ │ ├── label-studio-project-create.ts # 프로젝트 생성
860
+ │ └── index.ts
861
+ ├── README.md
862
+ ├── SETUP_GUIDE.md
863
+ ├── IMPLEMENTATION_GUIDE.md
864
+ └── package.json
865
+ ```
866
+
867
+ ## 애플리케이션 레벨 커스터마이징 예제
868
+
869
+ ### 1. server/index.ts에서 초기화
870
+
871
+ ```typescript
872
+ import {
873
+ registerWebhookHandler,
874
+ registerExportFormat,
875
+ WebhookAction,
876
+ LabelConfigTemplates,
877
+ TaskTransformTemplates
878
+ } from '@things-factory/integration-label-studio'
879
+
880
+ // 웹훅 핸들러 등록
881
+ registerWebhookHandler(WebhookAction.ANNOTATION_CREATED, async (payload, ctx) => {
882
+ // WBM 품질 시스템에 어노테이션 전송
883
+ await sendToWBMQualitySystem(payload.annotation)
884
+ })
885
+
886
+ // 커스텀 익스포트 포맷 등록
887
+ registerExportFormat('wbm-report', (annotations, context) => {
888
+ // WBM 전용 리포트 포맷
889
+ return generateWBMReport(annotations)
890
+ })
891
+ ```
892
+
893
+ ### 2. GraphQL Resolver에서 사용
894
+
895
+ ```typescript
896
+ import { LabelConfigBuilder, TaskTransformer } from '@things-factory/integration-label-studio'
897
+
898
+ @Mutation(returns => LabelStudioProject)
899
+ async createWBMProject(@Ctx() context: ResolverContext) {
900
+ // 템플릿으로 설정 생성
901
+ const config = LabelConfigTemplates.imageRankN(3, getWBMDefectTypes())
902
+ const xml = LabelConfigBuilder.build(config)
903
+
904
+ // Label Studio 프로젝트 생성
905
+ const project = await labelStudioApi.createProject({
906
+ title: 'WBM Defect Classification',
907
+ label_config: xml
908
+ })
909
+
910
+ return project
911
+ }
912
+
913
+ @Mutation(returns => TaskImportResult)
914
+ async importWBMImages(@Arg('deviceData') deviceData: string) {
915
+ const sourceData = JSON.parse(deviceData)
916
+
917
+ // WBM 디바이스 데이터를 Label Studio 태스크로 변환
918
+ const rule = TaskTransformTemplates.wbmImageClassification(true)
919
+ const lsTasks = TaskTransformer.transform(sourceData, rule)
920
+
921
+ // Label Studio에 임포트
922
+ return await labelStudioApi.importTasks(projectId, lsTasks)
923
+ }
924
+ ```
925
+
926
+ ### 3. 클라이언트에서 사용
927
+
928
+ ```typescript
929
+ // 프로젝트 생성 (선언적 사양 사용)
930
+ const result = await client.mutate({
931
+ mutation: gql`
932
+ mutation CreateWBMProject {
933
+ createLabelStudioProjectWithSpec(
934
+ input: { title: "WBM Line 1", labelConfigSpec: { dataType: "image", controls: "[...]" } }
935
+ ) {
936
+ id
937
+ }
938
+ }
939
+ `
940
+ })
941
+
942
+ // 디바이스 데이터 임포트 (변환 사용)
943
+ const importResult = await client.mutate({
944
+ mutation: gql`
945
+ mutation ImportWBMData($input: ImportTasksWithTransformInput!) {
946
+ importTasksWithTransform(projectId: 1, input: $input) {
947
+ imported
948
+ taskIds
949
+ }
950
+ }
951
+ `,
952
+ variables: {
953
+ input: {
954
+ sourceData: JSON.stringify(wbmDeviceData),
955
+ transformRule: wbmTransformRule
956
+ }
957
+ }
958
+ })
959
+ ```
960
+
961
+ ## 트러블슈팅
962
+
963
+ ### 웹훅이 수신되지 않음
964
+
965
+ **원인:** Label Studio가 Things-Factory에 접근 불가
966
+
967
+ **해결:**
968
+
969
+ ```bash
970
+ # Label Studio에서 Things-Factory URL 접근 가능한지 확인
971
+ curl -X POST https://your-things-factory.com/label-studio/webhook \
972
+ -H "Content-Type: application/json" \
973
+ -d '{"action":"TEST"}'
974
+ ```
975
+
976
+ ### 프로젝트 생성 실패
977
+
978
+ **원인:** Label Studio API 토큰 없음
979
+
980
+ **해결:**
981
+
982
+ ```bash
983
+ # Label Studio에서 API 토큰 발급
984
+ # Settings → Account → Access Token 복사
985
+ export LABEL_STUDIO_API_TOKEN=your-token
986
+ ```
987
+
988
+ ### CORS 에러
989
+
990
+ **원인:** Label Studio CORS 설정 누락
991
+
992
+ **해결:**
993
+ Label Studio settings에 추가:
994
+
995
+ ```python
996
+ CORS_ALLOWED_ORIGINS = [
997
+ 'https://your-things-factory.com',
998
+ ]
999
+ ```
1000
+
1001
+ ## 추가 문서
1002
+
1003
+ - [설치 및 설정 가이드](./SETUP_GUIDE.md)
1004
+ - [구현 가이드](./IMPLEMENTATION_GUIDE.md)
1005
+ - [UI 커스터마이징](./UI_CUSTOMIZATION.md)
1006
+ - [사용자 동기화](./USER_SYNC_GUIDE.md)
1007
+
1008
+ ## 라이선스
1009
+
1010
+ MIT
1011
+
1012
+ ## 기여
1013
+
1014
+ 버그 리포트 및 기능 제안은 GitHub Issues로 부탁드립니다.