@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.
- package/CHANGELOG.md +85 -0
- package/EXTERNAL_DATA_SOURCING.md +484 -0
- package/IMPLEMENTATION_GUIDE.md +469 -0
- package/INTEGRATION.md +279 -0
- package/README.md +1014 -0
- package/SETUP_GUIDE.md +577 -0
- package/TEST_GUIDE.md +387 -0
- package/UI_CUSTOMIZATION.md +395 -0
- package/USER_SYNC_GUIDE.md +514 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +1 -0
- package/client/label-studio-label-page.ts +52 -0
- package/client/label-studio-project-create.ts +216 -0
- package/client/label-studio-project-list.ts +214 -0
- package/client/label-studio-wrapper.ts +294 -0
- package/client/route.ts +15 -0
- package/client/tsconfig.json +13 -0
- package/config/config.development.js +124 -0
- package/config/config.production.js +182 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/label-studio-label-page.d.ts +8 -0
- package/dist-client/label-studio-label-page.js +54 -0
- package/dist-client/label-studio-label-page.js.map +1 -0
- package/dist-client/label-studio-project-create.d.ts +16 -0
- package/dist-client/label-studio-project-create.js +235 -0
- package/dist-client/label-studio-project-create.js.map +1 -0
- package/dist-client/label-studio-project-list.d.ts +16 -0
- package/dist-client/label-studio-project-list.js +222 -0
- package/dist-client/label-studio-project-list.js.map +1 -0
- package/dist-client/label-studio-wrapper.d.ts +57 -0
- package/dist-client/label-studio-wrapper.js +304 -0
- package/dist-client/label-studio-wrapper.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +14 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
- package/dist-server/controller/label-studio-role-mapper.js +65 -0
- package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
- package/dist-server/controller/user-provisioning-service.d.ts +66 -0
- package/dist-server/controller/user-provisioning-service.js +264 -0
- package/dist-server/controller/user-provisioning-service.js.map +1 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +19 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/route/label-studio-sso.d.ts +2 -0
- package/dist-server/route/label-studio-sso.js +156 -0
- package/dist-server/route/label-studio-sso.js.map +1 -0
- package/dist-server/route/webhook.d.ts +65 -0
- package/dist-server/route/webhook.js +248 -0
- package/dist-server/route/webhook.js.map +1 -0
- package/dist-server/route.d.ts +1 -0
- package/dist-server/route.js +21 -0
- package/dist-server/route.js.map +1 -0
- package/dist-server/service/ai-prediction-service.d.ts +27 -0
- package/dist-server/service/ai-prediction-service.js +222 -0
- package/dist-server/service/ai-prediction-service.js.map +1 -0
- package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
- package/dist-server/service/dataset-labeling-integration.js +512 -0
- package/dist-server/service/dataset-labeling-integration.js.map +1 -0
- package/dist-server/service/external-data-source-service.d.ts +78 -0
- package/dist-server/service/external-data-source-service.js +415 -0
- package/dist-server/service/external-data-source-service.js.map +1 -0
- package/dist-server/service/index.d.ts +12 -0
- package/dist-server/service/index.js +27 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/label-studio-sso-service.d.ts +38 -0
- package/dist-server/service/label-studio-sso-service.js +98 -0
- package/dist-server/service/label-studio-sso-service.js.map +1 -0
- package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
- package/dist-server/service/ml/ml-backend-service.js +153 -0
- package/dist-server/service/ml/ml-backend-service.js.map +1 -0
- package/dist-server/service/prediction/prediction-management.d.ts +32 -0
- package/dist-server/service/prediction/prediction-management.js +299 -0
- package/dist-server/service/prediction/prediction-management.js.map +1 -0
- package/dist-server/service/project/project-management.d.ts +36 -0
- package/dist-server/service/project/project-management.js +309 -0
- package/dist-server/service/project/project-management.js.map +1 -0
- package/dist-server/service/task/task-management.d.ts +42 -0
- package/dist-server/service/task/task-management.js +372 -0
- package/dist-server/service/task/task-management.js.map +1 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
- package/dist-server/service/webhook/webhook-management.d.ts +21 -0
- package/dist-server/service/webhook/webhook-management.js +134 -0
- package/dist-server/service/webhook/webhook-management.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/types/dataset-labeling-types.d.ts +71 -0
- package/dist-server/types/dataset-labeling-types.js +259 -0
- package/dist-server/types/dataset-labeling-types.js.map +1 -0
- package/dist-server/types/label-studio-types.d.ts +128 -0
- package/dist-server/types/label-studio-types.js +494 -0
- package/dist-server/types/label-studio-types.js.map +1 -0
- package/dist-server/types/prediction-types.d.ts +39 -0
- package/dist-server/types/prediction-types.js +121 -0
- package/dist-server/types/prediction-types.js.map +1 -0
- package/dist-server/utils/annotation-exporter.d.ts +104 -0
- package/dist-server/utils/annotation-exporter.js +261 -0
- package/dist-server/utils/annotation-exporter.js.map +1 -0
- package/dist-server/utils/label-config-builder.d.ts +117 -0
- package/dist-server/utils/label-config-builder.js +286 -0
- package/dist-server/utils/label-config-builder.js.map +1 -0
- package/dist-server/utils/label-studio-api-client.d.ts +180 -0
- package/dist-server/utils/label-studio-api-client.js +401 -0
- package/dist-server/utils/label-studio-api-client.js.map +1 -0
- package/dist-server/utils/media-url-extractor.d.ts +45 -0
- package/dist-server/utils/media-url-extractor.js +152 -0
- package/dist-server/utils/media-url-extractor.js.map +1 -0
- package/dist-server/utils/task-transformer.d.ts +108 -0
- package/dist-server/utils/task-transformer.js +260 -0
- package/dist-server/utils/task-transformer.js.map +1 -0
- package/package.json +47 -0
- package/server/SERVER_STRUCTURE.md +351 -0
- package/server/controller/label-studio-role-mapper.ts +76 -0
- package/server/controller/user-provisioning-service.ts +340 -0
- package/server/index.ts +19 -0
- package/server/route/label-studio-sso.ts +194 -0
- package/server/route/webhook.ts +304 -0
- package/server/route.ts +35 -0
- package/server/service/ai-prediction-service.ts +239 -0
- package/server/service/dataset-labeling-integration.ts +590 -0
- package/server/service/external-data-source-service.ts +438 -0
- package/server/service/index.ts +24 -0
- package/server/service/label-studio-sso-service.ts +108 -0
- package/server/service/labeling-scenario-service.ts.deprecated +566 -0
- package/server/service/ml/ml-backend-service.ts +127 -0
- package/server/service/prediction/prediction-management.ts +281 -0
- package/server/service/project/project-management.ts +284 -0
- package/server/service/task/task-management.ts +363 -0
- package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
- package/server/service/webhook/webhook-management.ts +109 -0
- package/server/tsconfig.json +11 -0
- package/server/types/dataset-labeling-types.ts +181 -0
- package/server/types/global.d.ts +23 -0
- package/server/types/label-studio-types.ts +346 -0
- package/server/types/prediction-types.ts +86 -0
- package/server/types/scenario-types.ts.deprecated +362 -0
- package/server/utils/annotation-exporter.ts +340 -0
- package/server/utils/label-config-builder.ts +340 -0
- package/server/utils/label-studio-api-client.ts +487 -0
- package/server/utils/media-url-extractor.ts +193 -0
- package/server/utils/task-transformer.ts +342 -0
- package/test-ai-prediction.js +268 -0
- package/test-dataset-integration.js +449 -0
- package/test-simple.js +89 -0
- 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
|
+
[](https://github.com/aidoop/label-studio-custom)
|
|
6
|
+
[](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로 부탁드립니다.
|