# Label Studio 사용자 동기화 가이드

Things-Factory의 사용자를 Label Studio에 동기화하는 방법을 설명합니다.

---

## 🔄 SSO vs 사용자 동기화

Things-Factory와 Label Studio의 통합은 **두 가지 독립적인 메커니즘**을 사용합니다:

### 1. SSO 자동 로그인 (`label-studio-sso` 패키지)
- **목적**: JWT 토큰을 통한 자동 로그인
- **방식**: iframe URL에 토큰 포함 → Label Studio에서 자동 인증
- **사용자 생성**: 선택적 (`JWT_SSO_AUTO_CREATE_USERS` 설정)
- **권한**: Label Studio 기본 권한 (Annotator)

### 2. 배치 사용자 동기화 (이 문서)
- **목적**: Things-Factory의 권한 체계를 Label Studio에 반영
- **방식**: Label Studio REST API를 통한 사용자 생성/업데이트
- **사용자 생성**: 항상 생성 (없으면)
- **권한**: Things-Factory 권한에 따른 Label Studio 역할 매핑

### 권장 구성

```
SSO 자동 로그인: JWT_SSO_AUTO_CREATE_USERS = False
                 ↓
배치 사용자 동기화: GraphQL Mutation으로 수동 동기화
                 ↓
Things-Factory 권한 → Label Studio 역할 반영
```

---

## 🎯 동기화 전략

### 핵심 원칙

1. **이메일 기반 매칭**: Things-Factory와 Label Studio 간 사용자는 **이메일로 매칭**
2. **권한 기반 필터링**: **Label Studio 권한이 있는 사용자만** 동기화
3. **배치 동기화**: 실시간이 아닌 **필요할 때 수동으로** GraphQL Mutation 실행
4. **3단계 권한 매핑**: Things-Factory Label Studio 권한 → Label Studio 기본 권한

```
Things-Factory User (email)
         ↓
Label Studio 권한 확인?
         ├─ YES → Label Studio에 동기화 (3단계 권한)
         └─ NO  → 동기화 안 함
```

---

## 📋 Label Studio 권한 체계

Things-Factory 사용자 권한이 Label Studio 권한으로 매핑됩니다:

| Things-Factory 권한 | Label Studio 권한 | 설명 |
|---------------------|------------------|------|
| `label-studio` 권한 없음 | Inactive | 비활성화 (is_active=false) |
| `label-studio` 권한 + Owner | Admin | 전체 관리 권한 (is_superuser=true, is_staff=true) |
| `label-studio` 권한 (일반) | Staff | 라벨링 작업만 (is_superuser=false, is_staff=false) |

### 권한별 상세 기능

#### 1. Admin (도메인 Owner + label-studio 권한)
- ✅ 모든 Annotation 생성/수정/삭제
- ✅ 프로젝트 생성/수정/삭제
- ✅ 사용자 관리 (추가/삭제/권한 변경)
- ✅ 조직 설정
- ✅ Django Admin 페이지 접근

#### 2. Staff (label-studio 권한)
- ✅ 자신의 Annotation 생성/수정/삭제
- ✅ 다른 사용자의 Annotation 보기
- ❌ 프로젝트 설정 변경 불가
- ❌ 사용자 관리 불가

#### 3. Inactive (권한 없음)
- ❌ Label Studio 접근 불가
- Label Studio에서 비활성화 처리

### 권한 부여 방법

1. **Things-Factory Admin에서 Role 생성**
   ```
   Role 이름: "Label Studio Staff"
   Privileges:
   - label-studio (category: label-studio, privilege: query 또는 mutation)
   ```

2. **사용자에게 Role 부여**
   ```
   User → Granted Roles → Add Role → "Label Studio Staff"
   ```

3. **동기화 실행** (GraphQL)
   ```graphql
   mutation {
     syncAllUsersToLabelStudio {
       total
       created
       updated
       deactivated
     }
   }
   ```

---

## 🔧 설정

### 1. Label Studio API Token 설정

Things-Factory에서 사용자 동기화를 위해 Label Studio API Token이 필요합니다.

#### Label Studio API Token 생성

1. Label Studio 로그인
2. **Account Settings** → **Access Token** 메뉴 이동
3. **"Create new token"** 클릭
4. 생성된 토큰 복사

#### Things-Factory 설정

**config/label-studio.config.js**:
```javascript
module.exports = {
  labelStudio: {
    serverUrl: process.env.LABEL_STUDIO_URL || 'http://localhost:8080',
    apiToken: process.env.LABEL_STUDIO_API_TOKEN || '',
    interfaces: 'panel,controls,annotations:menu'
  }
}
```

**환경 변수 (.env)**:
```bash
LABEL_STUDIO_URL=https://label-studio.example.com
LABEL_STUDIO_API_TOKEN=your-api-token-here
```

---

## 🚀 사용자 동기화

### Option 1: 개인 동기화 (본인만)

**권한 요구사항**: `label-studio` 카테고리에 `staff` 권한 또는 도메인 소유자

```graphql
mutation {
  syncMyUserToLabelStudio {
    success
    email
    action          # "created" | "updated" | "deactivated" | "skipped" | "error"
    lsUserId
    lsPermissions   # "Admin (Full access)" | "Staff (Labeling only)" | "Inactive"
    error
  }
}
```

**사용 케이스**:
- 사용자가 처음 Label Studio 권한을 받았을 때
- Label Studio 메뉴 접근 전 확인용

### Option 2: 전체 동기화 (관리자용)

**권한 요구사항**: `label-studio` 카테고리에 `admin` 권한 또는 도메인 소유자

```graphql
mutation {
  syncAllUsersToLabelStudio {
    total         # 전체 사용자 수
    created       # 새로 생성된 수
    updated       # 업데이트된 수
    deactivated   # 비활성화된 수
    skipped       # 건너뛴 수
    errors        # 에러 수

    results {
      success
      email
      action
      lsUserId
      lsPermissions
      error
    }
  }
}
```

**사용 케이스**:
- 초기 설정 후 모든 사용자 일괄 동기화
- 권한 변경 후 일괄 반영
- 주기적인 동기화 (예: 주 1회)

---

## 📊 동기화 시나리오

### 시나리오 1: 신규 사용자 추가 (Staff 권한)

```
1. Things-Factory에 사용자 생성
   User(email: 'john@example.com')

2. label-studio 권한이 있는 Role 부여
   Role: "Label Studio Staff"
   → Privilege: label-studio (query 또는 mutation)

3. 동기화 실행
   mutation { syncAllUsersToLabelStudio { ... } }

4. 결과
   ✅ Label Studio에 사용자 생성
   - email: john@example.com
   - username: john@example.com
   - is_superuser: false
   - is_staff: false
   - is_active: true
   → lsPermissions: "Staff (Labeling only)"
```

### 시나리오 2: 권한 업그레이드 (Staff → Admin)

```
1. Things-Factory에서 도메인 Owner로 설정
   User: john@example.com
   → owner: true

2. 동기화 실행
   mutation { syncAllUsersToLabelStudio { ... } }

3. 결과
   ✅ Label Studio에서 권한 업데이트
   - is_superuser: false → true
   - is_staff: false → true
   → lsPermissions: "Admin (Full access)"
```

### 시나리오 3: 권한 제거

```
1. Things-Factory에서 label-studio 권한 제거
   User: john@example.com
   → label-studio 권한 없음

2. 동기화 실행
   mutation { syncAllUsersToLabelStudio { ... } }

3. 결과
   ✅ Label Studio에서 비활성화
   - is_active: true → false
   → lsPermissions: "Inactive (No Label Studio access)"
```

### 시나리오 4: 이름 변경

```
1. Things-Factory에서 사용자 이름 변경
   User.name: "John Doe" → "Jane Doe"

2. 동기화 실행
   mutation { syncAllUsersToLabelStudio { ... } }

3. 결과
   ✅ Label Studio에서 이름 업데이트
   - first_name: "Jane"
   - last_name: "Doe"
```

---

## 🔍 동기화 로직 상세

### 동기화 판단 흐름

```typescript
for each user in Things-Factory domain:

  1. Label Studio 권한 확인
     hasLabelStudioPrivilege(user)?

     ├─ YES
     │   ↓
     │   2. Label Studio 역할 매핑
     │      lsRole = mapUserRole(user)
     │
     │   3. Label Studio API 호출
     │      ├─ 이메일로 사용자 조회
     │      ├─ 존재하면 → UPDATE
     │      └─ 없으면 → CREATE
     │
     │   4. Organization 멤버십 설정
     │      role = lsRole
     │
     └─ NO
         ↓
         5. Label Studio에서 비활성화
            is_active = false
```

### 역할 매핑 로직

```typescript
// Things-Factory 사용자의 권한 분석
const hasLabelStudioPrivilege =
  await User.hasPrivilege('label-studio', 'query', domain, user) ||
  await User.hasPrivilege('label-studio', 'mutation', domain, user)

if (!hasLabelStudioPrivilege) {
  // Label Studio 권한 없음 → 비활성화
  return {
    is_superuser: false,
    is_staff: false,
    is_active: false
  }
}

if (user.owner === true) {
  // 도메인 Owner + Label Studio 권한 → Admin
  return {
    is_superuser: true,
    is_staff: true,
    is_active: true
  }
}

// Label Studio 권한은 있지만 Owner 아님 → Staff
return {
  is_superuser: false,
  is_staff: false,
  is_active: true
}
```

---

## 📈 모니터링 및 로그

### 동기화 결과 확인

```graphql
mutation {
  syncAllUsersToLabelStudio {
    total
    created
    updated
    deactivated
    skipped
    errors

    # 상세 결과
    results {
      email
      action
      error
    }
  }
}
```

### 예상 출력

```json
{
  "data": {
    "syncAllUsersToLabelStudio": {
      "total": 50,
      "created": 5,
      "updated": 40,
      "deactivated": 3,
      "skipped": 0,
      "errors": 2,
      "results": [
        {
          "email": "john@example.com",
          "action": "created",
          "error": null
        },
        {
          "email": "jane@example.com",
          "action": "updated",
          "error": null
        },
        {
          "email": "bob@example.com",
          "action": "error",
          "error": "API connection timeout"
        }
      ]
    }
  }
}
```

### 서버 로그

```
🔄 Starting batch sync for 50 users...
✅ Created privilege: label-studio:viewer
✅ User synced: john@example.com (created)
✅ User synced: jane@example.com (updated)
❌ Failed to sync: bob@example.com (API timeout)
✅ Batch sync completed: {
  total: 50,
  created: 5,
  updated: 40,
  errors: 2
}
```

---

## 🛠️ 문제 해결

### 1. "Label Studio API token is not configured"

**원인**: config 파일이나 환경 변수에 API Token이 설정되지 않음

**해결**:
```bash
# .env 파일에 추가
LABEL_STUDIO_API_TOKEN=your-api-token-here
```

또는 `config/label-studio.config.js`에 직접 설정:
```javascript
module.exports = {
  labelStudio: {
    apiToken: 'your-api-token-here'
  }
}
```

### 2. 사용자가 동기화되지 않음

**원인**: `label-studio` 권한이 없음

**확인**:
1. Things-Factory에서 사용자의 Granted Roles 확인
2. 해당 Role의 Privileges에 `label-studio` 카테고리 권한이 있는지 확인

**해결**:
```
User → Granted Roles → Add Role → (label-studio 권한이 있는 Role)
```

### 3. API 호출 실패

**원인**: 네트워크 문제 또는 Label Studio 서버 다운

**확인**:
```bash
curl -H "Authorization: Token YOUR_TOKEN" \
  https://label-studio.example.com/api/users
```

**해결**:
- Label Studio 서버 상태 확인
- 네트워크 연결 확인
- API Token 유효성 확인

### 4. 권한 에러 발생

**원인**: GraphQL 뮤테이션 실행 권한 부족

**확인**:
- `syncMyUserToLabelStudio`: `label-studio` 카테고리에 `staff` 권한 필요
- `syncAllUsersToLabelStudio`: `label-studio` 카테고리에 `admin` 권한 필요

**해결**:
적절한 권한을 가진 사용자로 재로그인하거나 권한 부여

---

## ⏰ 권장 동기화 주기

### 수동 동기화 (권장)

```
이벤트 기반:
├─ 초기 설정 후 → 전체 동기화 1회
├─ 신규 사용자 추가 시 → 개인 동기화
├─ 권한 변경 시 → 전체 또는 개인 동기화
└─ 정기 점검 (월 1회) → 전체 동기화
```

### 자동화 (선택)

Things-Factory의 스케줄러를 사용하여 주기적 동기화 가능:

```typescript
// 매주 일요일 오전 2시 동기화
schedule('0 2 * * 0', async () => {
  await syncAllUsersToLabelStudio()
})
```

---

## 📚 관련 문서

- [README.md](./README.md) - 모듈 개요
- [Label Studio User Management](https://labelstud.io/guide/manage_users.html)
- [Things-Factory Role & Privilege](https://github.com/hatiolab/things-factory)

---

**문서 버전**: 1.0
**작성일**: 2025-10-01
**업데이트**: 2025-10-01
