# 조직과 워크스페이스

> TypeScript로 Nodaro 조직과 워크스페이스를 관리합니다. 학교와 팀을 만들고 멤버를 초대하며, 참여 코드를 발급하고 크레딧 사용량 보고서를 읽습니다.

Source: https://nodaro.ai/ko/docs/developers/sdk/organizations

<strong>`client.organizations`</strong>는 학교나 팀인 조직을 멤버, 초대, 감사 로그, 사용량 보고서와 함께 관리합니다. <strong>`client.workspaces`</strong>는 그 안의 워크스페이스(수업이나 팀 프로젝트 등)를 멤버, 참여 코드와 함께 관리합니다. 이 메서드들은 [워크스페이스 REST API](https://nodaro.ai/docs/developers/api/workspaces)를 호출합니다. 개념은 [워크스페이스](https://nodaro.ai/docs/concepts/workspaces)를 참조하세요.

조직은 Nodaro Cloud 기능이며, 인스턴스별로 켭니다. 조직이 없는 인스턴스에서는 이 페이지의 모든 메서드가 `NotFoundError`를 발생시킵니다.

## 소속과 동작
워크스페이스에 속하는 것과 워크스페이스에서 동작하는 것은 서로 다릅니다. 이 두 리소스는 누가 어디에 속하는지를 관리합니다. 워크스페이스에서 **동작**하려면, 즉 목록을 그 워크스페이스에서 읽고 새로 만든 결과물이 그곳에 저장되게 하려면, [`client.withWorkspace(workspaceId)`](https://nodaro.ai/docs/developers/sdk/client#withworkspaceworkspaceid)로 클라이언트를 만들거나 `createClient`에 `workspaceId`를 전달하세요.

```ts
const classroom = client.withWorkspace(workspaceId)
await classroom.workflows.run(workflowId) // runs in the class
await client.workflows.run(workflowId)    // runs in your personal space
```

[`client.me()`](https://nodaro.ai/docs/developers/sdk/client#me)는 내가 속한 조직과 워크스페이스, 그리고 `lastWorkspaceId`를 반환합니다.

모든 권한은 서버가 결정합니다. 호출자가 초대, 제거, 이름 변경을 할 수 있는지는 서버가 판단해 오류로 알려 줍니다. 설정에 따라 결과가 달라질 수 있으므로 내 클라이언트에서 짐작하지 마세요.

## 메서드
| 메서드 | 하는 일 |
| --- | --- |
| [`organizations.list()`](#organizationslist) | 내가 속한 조직을 나열합니다 |
| [`organizations.get(id)`](#organizationsgetid) | 조직 하나를 읽습니다 |
| [`organizations.create(input)`](#organizationscreateinput) | 학교나 팀을 만듭니다 |
| [`organizations.update(id, input)`](#organizationsupdateid-input) | 이름이나 설정을 바꿉니다 |
| [`organizations.delete(id)`](#organizationsdeleteid) | 조직을 삭제합니다 |
| [`organizations.transferOwnership(id, userId)`](#organizationstransferownershipid-userid) | 다른 멤버를 소유자로 지정합니다 |
| [`organizations.leave(id)`](#organizationsleaveid) | 조직에서 나갑니다 |
| [`organizations.listMembers()`, `updateMember()`, `removeMember()`](#organization-members) | 멤버를 관리합니다 |
| [`organizations.invite(orgId, input)`](#organizationsinviteorgid-input) | 이메일로 사람을 초대합니다 |
| [`organizations.listInvitations()`, `revokeInvitation()`, `resendInvitation()`](#manage-invitations) | 보낸 초대를 관리합니다 |
| [`organizations.previewInvitation()`, `acceptInvitation()`](#preview-and-accept-an-invitation) | 초대를 표시하고 수락합니다 |
| [`organizations.audit(orgId, opts?)`](#organizationsauditorgid-opts) | 감사 로그를 읽습니다 |
| [`organizations.usage()`, `usageRows()`, `usageCsv()`](#organization-usage) | 크레딧 사용량을 읽습니다 |
| [`workspaces.list()`](#workspaceslist) | 내가 속한 워크스페이스를 나열합니다 |
| [`workspaces.listForOrg(orgId, opts?)`](#workspaceslistfororgorgid-opts) | 조직의 워크스페이스를 나열합니다 |
| [`workspaces.get(id)`](#workspacesgetid) | 워크스페이스 하나를 읽습니다 |
| [`workspaces.create(orgId, input)`](#workspacescreateorgid-input) | 워크스페이스를 만듭니다 |
| [`workspaces.update(id, input)`](#workspacesupdateid-input) | 워크스페이스를 변경합니다 |
| [`workspaces.setArchived(id, archived)`](#workspacessetarchivedid-archived) | 워크스페이스를 보관하거나 복원합니다 |
| [`workspaces.listMembers()`, `addMember()`, `updateMember()`, `removeMember()`](#workspace-members) | 워크스페이스 멤버를 관리합니다 |
| [`workspaces.getJoinCode()`, `actOnJoinCode()`](#join-codes) | 참여 코드를 관리합니다 |
| [`workspaces.join(code)`](#workspacesjoincode) | 코드로 워크스페이스에 참여합니다 |
| [`workspaces.usage()`, `usageRows()`, `usageCsv()`](#workspace-usage) | 워크스페이스의 크레딧 사용량을 읽습니다 |

## client.organizations
### organizations.list()
내가 속한 조직을 나열합니다(`GET /v1/orgs`).

```ts
list(): Promise<{ data: OrganizationView[] }>
```

```ts
const { data: orgs } = await client.organizations.list()
```

### organizations.get(id)
조직 하나를 읽습니다(`GET /v1/orgs/:id`).

```ts
get(id: string): Promise<{ data: OrganizationView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "조직 ID입니다." },
}}
/>

```ts
const { data: org } = await client.organizations.get(orgId)
console.log(org.status) // pending, active, suspended or deleted
```

### organizations.create(input)
나를 소유자로 하는 조직을 만듭니다(`POST /v1/orgs`). 새 조직은 보통 `pending` 상태로 시작하고, Nodaro가 승인하면 `active`가 됩니다. pending 상태인 동안에는 나만 조직을 볼 수 있고 조직 안의 어떤 것도 변경할 수 없으므로, 사용자에게 승인을 기다리는 중이라고 알려 주세요.

```ts
create(input: { name: string; kind: "school" | "team"; slug?: string; acceptTerms?: boolean; settings?: OrgSettings }): Promise<{ data: OrganizationView }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "조직의 이름입니다." },
kind: { type: '"school" | "team"', required: true, description: "조직의 종류입니다. 기본 설정을 정합니다." },
slug: { type: 'string', description: "고유한 짧은 이름입니다. 이미 사용 중인 슬러그는 409 name_taken으로 실패합니다." },
acceptTerms: { type: 'boolean', description: "학교에는 필수입니다. 소유자가 학생을 등록할 권한이 있음을 확인합니다." },
settings: { type: 'OrgSettings', description: "조직의 모든 워크스페이스에 적용되는 설정입니다." },
}}
/>

```ts
const { data: school } = await client.organizations.create({
name: "Sunrise School",
kind: "school",
acceptTerms: true,
})
```

`acceptTerms: true`가 없는 학교는 `400 terms_required`로 실패합니다.

### organizations.update(id, input)
조직의 이름을 바꾸거나 설정을 변경합니다(`PATCH /v1/orgs/:id`).

```ts
update(id: string, input: { name?: string; settings?: OrgSettings }): Promise<{ data: OrganizationView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "조직 ID입니다." },
name: { type: 'string', description: "새 이름입니다." },
settings: { type: 'OrgSettings', description: "변경할 설정입니다. 아래 설정 표를 참조하세요." },
}}
/>

```ts
await client.organizations.update(orgId, {
settings: { default_workflow_visibility: "workspace", allowed_email_domains: ["school.example"] },
})
```

설정은 계층으로 적용됩니다. 워크스페이스 설정이 조직 설정보다 우선하고, 조직 설정은 그 종류의 기본값보다 우선합니다. 키를 `false`로 설정하는 것은 실제 값이며, “설정 안 함”이 아닙니다.

| 키 | 타입 | 의미 |
| --- | --- | --- |
| `admin_access` | `view` 또는 `edit` | 관리자가 멤버의 워크플로로 할 수 있는 일 |
| `default_workflow_visibility` | `private` 또는 `workspace` | 새 워크플로의 공개 범위 |
| `member_access_to_shared` | `view` 또는 `edit` | 멤버가 워크스페이스에 공유된 워크플로로 할 수 있는 일 |
| `members_can_create_projects` | 불리언 | 멤버가 워크스페이스에서 프로젝트를 만들 수 있는지 여부 |
| `member_caps_enabled` | 불리언 | 멤버별 크레딧 한도를 적용할지 여부 |
| `personal_space_enabled` | 불리언 | 멤버가 개인 공간을 가질 수 있는지 여부 |
| `workspace_admins_can_invite` | 불리언 | 워크스페이스 관리자가 새로운 사람을 초대할 수 있는지 여부 |
| `collaborators_can_invite` | 불리언 | 편집자 권한의 공동 작업자가 다른 공동 작업자를 초대할 수 있는지 여부 |
| `allowed_email_domains` | 문자열 목록 | 조직 전용: 참여할 수 있는 이메일 도메인 |
| `vocabulary_overrides` | 객체 | 조직 전용: 해당 종류의 단어에 쓸 새 레이블. 예: `{ "workspace": "Cohort" }`. 워크스페이스를 가리키는 단어가 문법상 남성형이면 `"workspace_gender": "m"`을 추가하세요. 그러면 히브리어와 브라질 포르투갈어 문장이 그 단어에 맞게 활용됩니다 |

조직 전용 키 2개는 업데이트할 때 전체가 교체됩니다.

### organizations.delete(id)
조직을 삭제합니다(`DELETE /v1/orgs/:id`). 이전 멤버에게는 조직이 숨겨지지만, 아무것도 파기되지 않습니다. 먼저 모든 워크스페이스를 보관하세요. 그렇지 않으면 호출이 `409 has_active_workspaces`로 실패합니다.

```ts
delete(id: string): Promise<{ data: { id: string; status: string } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "조직 ID입니다." },
}}
/>

```ts
await client.organizations.delete(orgId)
```

### organizations.transferOwnership(id, userId)
다른 멤버를 소유자로 지정합니다(`POST /v1/orgs/:id/transfer-ownership`). 나는 관리자가 됩니다.

```ts
transferOwnership(id: string, userId: string): Promise<{ data: { orgId: string; ownerUserId: string } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "조직 ID입니다." },
userId: { type: 'string', required: true, description: "소유자가 될 멤버입니다." },
}}
/>

```ts
await client.organizations.transferOwnership(orgId, newOwnerId)
```

### organizations.leave(id)
조직에서 나갑니다(`POST /v1/orgs/:id/leave`). 소유자는 나갈 수 없습니다. 먼저 소유권을 이전하세요. 그렇지 않으면 호출이 `409 owner_cannot_leave`로 실패합니다.

```ts
leave(id: string): Promise<{ data: { orgId: string; left: boolean } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "조직 ID입니다." },
}}
/>

```ts
await client.organizations.leave(orgId)
```

### 조직 멤버
조직의 멤버를 나열하고, 변경하고, 제거합니다(`/v1/orgs/:id/members`). 멤버는 `active` 또는 `suspended` 상태입니다. 정지된 멤버는 자리를 유지하지만 동작할 수 없습니다. 소유자는 정지하거나, 제거하거나, 강등할 수 없습니다.

```ts
listMembers(orgId: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<OrgMemberView>>
updateMember(orgId: string, userId: string, input: { role?: "admin" | "member"; status?: "active" | "suspended" }): Promise<{ data: OrgMemberView }>
removeMember(orgId: string, userId: string): Promise<{ data: { removed: boolean } }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "조직 ID입니다." },
userId: { type: 'string', description: "대상 멤버입니다. updateMember와 removeMember에서는 필수입니다." },
cursor: { type: 'string', description: "listMembers: 이전 페이지의 nextCursor입니다." },
limit: { type: 'number', description: "listMembers: 페이지 크기입니다." },
role: { type: '"admin" | "member"', description: "updateMember: 새 역할입니다." },
status: { type: '"active" | "suspended"', description: "updateMember: 멤버를 정지하거나 다시 활성화합니다." },
}}
/>

```ts
const { data: members, nextCursor } = await client.organizations.listMembers(orgId)
await client.organizations.updateMember(orgId, userId, { role: "admin" })
```

### organizations.invite(orgId, input)
이메일로 사람을 초대합니다(`POST /v1/orgs/:id/invitations`). **주소마다 행 하나**를 반환합니다.

```ts
invite(orgId: string, input: {
emails: string[]
orgRole?: "admin" | "member"
workspaceId?: string
workspaceRole?: "admin" | "member"
}): Promise<{ data: InvitationDelivery[] }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "조직 ID입니다." },
emails: { type: 'string[]', required: true, description: "초대할 주소입니다." },
orgRole: { type: '"admin" | "member"', description: "조직에서의 역할입니다." },
workspaceId: { type: 'string', description: "초대를 수락했을 때 그 사람을 추가할 워크스페이스입니다." },
workspaceRole: { type: '"admin" | "member"', description: "그 워크스페이스에서의 역할입니다." },
}}
/>

```ts
const { data: rows } = await client.organizations.invite(orgId, {
emails: ["teacher@school.example"],
workspaceId,
workspaceRole: "admin",
})
for (const row of rows) {
if (row.status !== "sent") showInviteLink(row.email, row.link) // not emailed: share the link yourself
}
```

**링크를 보여 주세요.** `status`가 `sent`가 아닌 행에는 대신 `link`가 들어 있습니다. 설치에 메일 공급자가 없거나 전송에 실패한 경우입니다. 어느 쪽이든 초대는 존재하며, 링크가 없으면 아무도 초대에 접근할 수 없습니다. 초대는 14일 후에 만료됩니다. 조직의 하루 초대 한도에 도달하면 `RateLimitedError`가 반환됩니다.

### 초대 관리
초대를 나열하고, 취소하고, 다시 보냅니다.

```ts
listInvitations(orgId: string, opts?: { status?: "open" | "accepted" | "revoked" | "expired"; workspaceId?: string; cursor?: string; limit?: number }): Promise<OrgPage<InvitationView>>
revokeInvitation(id: string): Promise<{ data: { id: string; revoked: boolean } }>
resendInvitation(id: string): Promise<{ data: InvitationDelivery & { id: string } }>
```

<TypeTable
type={{
orgId: { type: 'string', description: "listInvitations: 조직 ID입니다." },
id: { type: 'string', description: "revokeInvitation과 resendInvitation: 초대 ID입니다." },
status: { type: '"open" | "accepted" | "revoked" | "expired"', description: "listInvitations: 이 상태의 초대만 반환합니다." },
workspaceId: { type: 'string', description: "listInvitations: 이 워크스페이스로의 초대만 반환합니다." },
cursor: { type: 'string', description: "listInvitations: 이전 페이지의 nextCursor입니다." },
limit: { type: 'number', description: "listInvitations: 페이지 크기입니다." },
}}
/>

```ts
const { data: open } = await client.organizations.listInvitations(orgId, { status: "open" })
await client.organizations.resendInvitation(open[0].id)
```

### 초대 미리 보기와 수락
`previewInvitation()`은 **공개**입니다. 초대받은 사람이 아직 로그인하지 않은 상태에서도 동작하며, 주소를 가려서 반환합니다. `acceptInvitation()`에는 초대의 이메일과 일치하는 이메일로 로그인한 호출자가 필요합니다.

```ts
previewInvitation(token: string): Promise<{ data: InvitationPreview }>
acceptInvitation(token: string): Promise<{ data: { orgId: string; workspaceId: string | null } }>
```

<TypeTable
type={{
token: { type: 'string', required: true, description: "초대 링크에 있는 토큰입니다." },
}}
/>

```ts
const { data: preview } = await client.organizations.previewInvitation(token)
const { data: joined } = await signedInClient.organizations.acceptInvitation(token)
```

수락은 `400 invitation_expired`, `invitation_revoked`, `invitation_accepted`, `email_mismatch`로 실패할 수 있으며, 조직이 목록에 있는 이메일 도메인만 받는 경우에는 `ForbiddenError`로 실패합니다.

### organizations.audit(orgId, opts?)
조직의 감사 로그를 최신 항목부터 읽습니다(`GET /v1/orgs/:id/audit`). 조직이 정지된 동안에도 계속 읽을 수 있습니다.

```ts
audit(orgId: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<OrgAuditEntry>>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "조직 ID입니다." },
cursor: { type: 'string', description: "이전 페이지의 nextCursor입니다." },
limit: { type: 'number', description: "페이지 크기입니다." },
}}
/>

```ts
const { data: entries } = await client.organizations.audit(orgId, { limit: 50 })
for (const e of entries) console.log(e.createdAt, e.action, e.actor?.displayName ?? "system")
```

각 항목에는 `id`, `workspaceId`, `action`, `targetType`, `targetId`, `details`, `createdAt`, `actor`가 있으며, 시스템이 수행한 동작이면 `actor`는 `null`입니다. `action`은 **열린** 목록입니다. 알고 있는 동작은 표시하고, 나머지는 원래 문자열을 그대로 표시하세요. 고정된 목록만 처리하는 코드는 새 동작이 처음 나타날 때 깨집니다.

### 조직 사용량
소유자와 조직 관리자를 위한, 기간별 크레딧 사용량 보고서입니다(`GET /v1/orgs/:id/usage`). `usage()`는 보고서를 그룹으로 묶고, `usageRows()`는 그 보고서를 이루는 실행을 최신 항목부터 한 페이지씩 반환하며, `usageCsv()`는 보고서나 행을 CSV 텍스트로 반환합니다.

```ts
usage(orgId: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: "workspace" | "member" | "model" | "day"; workspaceId?: string; userId?: string }): Promise<{ data: UsageReport }>
usageRows(orgId: string, opts?: { from?: string; to?: string; tz?: string; workspaceId?: string; userId?: string; cursor?: string; limit?: number }): Promise<OrgPage<UsageLogEntry>>
usageCsv(orgId: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: string; workspaceId?: string; userId?: string }): Promise<string>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "조직 ID입니다." },
from: { type: 'string', description: "기간의 첫날이며, 그날을 포함합니다." },
to: { type: 'string', description: "기간의 마지막 날이며, 그날을 포함합니다. 기간은 최대 366일입니다." },
tz: { type: 'string', description: "날짜를 나누는 기준이 되는 IANA 시간대입니다. 예: Europe/Rome." },
groupBy: { type: '"workspace" | "member" | "model" | "day"', description: "usage와 usageCsv: 보고서를 묶는 기준입니다." },
workspaceId: { type: 'string', description: "워크스페이스 하나로 한정합니다." },
userId: { type: 'string', description: "멤버 한 명으로 한정합니다." },
cursor: { type: 'string', description: "usageRows: 이전 페이지의 nextCursor입니다." },
limit: { type: 'number', description: "usageRows: 페이지 크기입니다." },
}}
/>

```ts
const { data: report } = await client.organizations.usage(orgId, {
from: "2026-09-01",
to: "2026-09-30",
tz: "Europe/Rome",
groupBy: "workspace",
})
const csv = await client.organizations.usageCsv(orgId, { from: "2026-09-01", to: "2026-09-30" })
```

각 행은 세 가지 크레딧 수치를 보고합니다. `credits`는 지금까지 실행에 든 크레딧입니다. 끝난 실행은 확정된 금액이고, 그 밖의 실행은 예약된 금액입니다. `settledCredits`와 `inFlightCredits`는 이 값을 나눈 것입니다. 그룹이 `truncated`인 경우에도 합계는 기간 전체를 포함합니다.

- `platformAbsorbedCredits`는 종량제 실행이 워크스페이스의 남은 예산을 넘어선 초과분으로, 플랫폼이 부담합니다.
- `chargedToBudget`은 `settledCredits`에서 `platformAbsorbedCredits`를 뺀 값으로, 예산에 반영된 확정 크레딧입니다.
- `appMarkupAbsorbedCredits`는 예산으로 충당하지 못한 앱 추가 요금입니다. 보고서에 해당하는 실행이 없으므로 `chargedToBudget`에 포함되지 않습니다.

CSV 내보내기는 사용자당 분당 10회로 제한됩니다.

## client.workspaces
### workspaces.list()
내가 속한 워크스페이스와 내 `lastWorkspaceId`를 나열합니다(`GET /v1/workspaces`). 요약 정보를 반환하며, `client.me()`에 담긴 목록과 같습니다.

```ts
list(): Promise<{ data: WorkspaceSummary[]; lastWorkspaceId: string | null }>
```

```ts
const { data: workspaces, lastWorkspaceId } = await client.workspaces.list()
```

### workspaces.listForOrg(orgId, opts?)
조직 하나의 워크스페이스를 나열합니다(`GET /v1/orgs/:id/workspaces`).

```ts
listForOrg(orgId: string, opts?: { includeArchived?: boolean }): Promise<{ data: WorkspaceView[] }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "조직 ID입니다." },
includeArchived: { type: 'boolean', default: 'false', description: "보관된 워크스페이스도 포함합니다." },
}}
/>

```ts
const { data: classes } = await client.workspaces.listForOrg(orgId)
```

### workspaces.get(id)
워크스페이스 하나의 전체 정보를 읽습니다(`GET /v1/workspaces/:id`).

```ts
get(id: string): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "워크스페이스 ID입니다." },
}}
/>

```ts
const { data: workspace } = await client.workspaces.get(workspaceId)
```

### workspaces.create(orgId, input)
조직 안에 워크스페이스를 만듭니다(`POST /v1/orgs/:id/workspaces`). 조직이 active 상태여야 합니다.

```ts
create(orgId: string, input: { name: string; slug?: string; description?: string; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "조직 ID입니다." },
name: { type: 'string', required: true, description: "워크스페이스 이름입니다." },
slug: { type: 'string', description: "고유한 짧은 이름입니다." },
description: { type: 'string', description: "설명입니다." },
settings: { type: 'WorkspaceSettings', description: "조직의 설정보다 우선하는 설정입니다." },
}}
/>

```ts
const { data: classOne } = await client.workspaces.create(orgId, { name: "Class 1" })
```

### workspaces.update(id, input)
워크스페이스를 변경합니다(`PATCH /v1/workspaces/:id`).

```ts
update(id: string, input: { name?: string; description?: string | null; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "워크스페이스 ID입니다." },
name: { type: 'string', description: "새 이름입니다." },
description: { type: 'string | null', description: "새 설명입니다. 설명을 없애려면 null을 전달하세요." },
settings: { type: 'WorkspaceSettings', description: "변경할 설정입니다." },
}}
/>

```ts
await client.workspaces.update(workspaceId, { settings: { members_can_create_projects: true } })
```

### workspaces.setArchived(id, archived)
워크스페이스를 보관하거나 복원합니다(`POST /v1/workspaces/:id/archive` 또는 `/unarchive`). 보관은 되돌릴 수 있으며 아무것도 파기하지 않습니다. 워크스페이스는 새 작업을 받지 않게 되지만, 모든 내용을 계속 읽을 수 있습니다. 보관된 워크스페이스에 쓰기를 시도하면 `ForbiddenError`로 실패합니다.

```ts
setArchived(id: string, archived: boolean): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "워크스페이스 ID입니다." },
archived: { type: 'boolean', required: true, description: "true는 보관하고, false는 복원합니다." },
}}
/>

```ts
await client.workspaces.setArchived(workspaceId, true)
```

### 워크스페이스 멤버
워크스페이스의 멤버를 나열하고, 추가하고, 변경하고, 제거합니다(`/v1/workspaces/:id/members`). 추가하려면 그 사람이 이미 조직에 속해 있어야 합니다. 새로운 사람을 들이려면 초대하세요. 조직의 소유자와 관리자는 목록에 없어도 모든 워크스페이스의 관리자입니다.

```ts
listMembers(id: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<WorkspaceMemberView>>
addMember(id: string, input: { userId: string; role: "admin" | "member" }): Promise<{ data: WorkspaceMemberView }>
updateMember(id: string, userId: string, input: { role?: "admin" | "member"; status?: "active" | "suspended"; creditCap?: number | null }): Promise<{ data: WorkspaceMemberView }>
removeMember(id: string, userId: string): Promise<{ data: { removed: boolean } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "워크스페이스 ID입니다." },
userId: { type: 'string', description: "대상 멤버입니다. addMember, updateMember, removeMember에서는 필수입니다." },
role: { type: '"admin" | "member"', description: "워크스페이스 역할입니다." },
status: { type: '"active" | "suspended"', description: "updateMember: 이 워크스페이스에서 멤버를 정지하거나 다시 활성화합니다." },
creditCap: { type: 'number | null', description: "updateMember: 이 멤버의 크레딧 한도입니다. 한도를 두지 않으려면 null을 전달하세요." },
cursor: { type: 'string', description: "listMembers: 이전 페이지의 nextCursor입니다." },
limit: { type: 'number', description: "listMembers: 페이지 크기입니다." },
}}
/>

```ts
await client.workspaces.addMember(workspaceId, { userId, role: "member" })
await client.workspaces.updateMember(workspaceId, userId, { creditCap: 500 })
```

조직의 활성 멤버가 아닌 사람을 추가하면 `400 not_org_member`로 실패하고, 같은 사람을 두 번 추가하면 `409 already_a_member`로 실패합니다.

### 참여 코드
참여 코드가 있으면 초대 없이 워크스페이스에 참여할 수 있습니다. 코드를 읽거나 변경할 수 있는 사람은 워크스페이스 관리자뿐입니다.

```ts
getJoinCode(id: string): Promise<{ data: JoinCodeView | null }>
actOnJoinCode(id: string, action: "rotate" | "enable" | "disable"): Promise<{ data: JoinCodeView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "워크스페이스 ID입니다." },
action: { type: '"rotate" | "enable" | "disable"', description: "actOnJoinCode: rotate는 새 코드를 만들고 이전 코드를 즉시 중지합니다. enable과 disable은 코드를 켜고 끕니다." },
}}
/>

```ts
const { data: code } = await client.workspaces.actOnJoinCode(workspaceId, "enable")
console.log(code.code) // for example BCDFGHJK
```

아직 코드를 만든 적이 없으면 `getJoinCode()`는 `null`을 반환합니다.

### workspaces.join(code)
참여 코드로 워크스페이스에 참여합니다(`POST /v1/workspaces/join`). 내 클라이언트가 어느 워크스페이스에서 동작하든 작동하므로, 오래된 선택 때문에 막히는 일이 없습니다.

```ts
join(code: string): Promise<{ data: { orgId: string; workspaceId: string } }>
```

<TypeTable
type={{
code: { type: 'string', required: true, description: "참여 코드입니다." },
}}
/>

```ts
const { data: joined } = await client.workspaces.join("BCDFGHJK")
```

알 수 없는 코드, 꺼진 코드, 보관된 워크스페이스의 코드는 모두 `400 join_code_invalid`로 실패합니다. 세 경우 모두 같은 응답이므로, 코드로 무엇이 존재하는지 알아낼 수 없습니다. 시도가 너무 많으면 `RateLimitedError`가 반환됩니다.

### 워크스페이스 사용량
조직과 같은 크레딧 보고서를 워크스페이스 하나에 대해 제공합니다(`GET /v1/workspaces/:id/usage`). 멤버는 자신의 실행을 보고, 관리자는 모든 사람의 실행을 보며 `userId`로 필터링할 수 있습니다.

```ts
usage(id: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: "member" | "model" | "day"; userId?: string }): Promise<{ data: UsageReport }>
usageRows(id: string, opts?: { from?: string; to?: string; tz?: string; userId?: string; cursor?: string; limit?: number }): Promise<OrgPage<UsageLogEntry>>
usageCsv(id: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: string; userId?: string }): Promise<string>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "워크스페이스 ID입니다." },
from: { type: 'string', description: "첫날이며, 그날을 포함합니다." },
to: { type: 'string', description: "마지막 날이며, 그날을 포함합니다." },
tz: { type: 'string', description: "IANA 시간대입니다." },
groupBy: { type: '"member" | "model" | "day"', description: "보고서를 묶는 기준입니다." },
userId: { type: 'string', description: "관리자 전용: 멤버 한 명으로 한정합니다." },
cursor: { type: 'string', description: "usageRows: 이전 페이지의 nextCursor입니다." },
limit: { type: 'number', description: "usageRows: 페이지 크기입니다." },
}}
/>

```ts
const { data: byModel } = await client.workspaces.usage(workspaceId, { groupBy: "model" })
```

## 오류
모든 권한은 서버가 결정하고, 오류로 응답합니다. SDK에서는 403이 `ForbiddenError`, 404가 `NotFoundError`, 429가 `RateLimitedError`가 됩니다. 이유는 각 오류의 `message`에서 확인하세요. 그 밖의 상태는 서버의 `code`를 담은 `NodaroError`로 전달됩니다.

| 상태 | 코드 | 발생 조건 |
| --- | --- | --- |
| 400 | `validation_error` | 필드가 잘못되었거나, 커서 형식이 잘못되었거나, 사용량 보고서의 날짜, 시간대 또는 기간이 잘못되었습니다 |
| 400 | `terms_required` | `acceptTerms: true` 없이 학교를 만들었습니다 |
| 400 | `not_org_member` | 워크스페이스에 추가한 사람이 그 조직의 활성 멤버가 아닙니다 |
| 400 | `token_workspace_mismatch` | 한 워크스페이스에 묶인 토큰을 다른 워크스페이스에서 사용했습니다 |
| 400 | `join_code_invalid` | 참여 코드가 없거나, 꺼져 있거나, 보관된 워크스페이스의 코드입니다 |
| 400 | `invitation_expired`, `invitation_revoked`, `invitation_accepted`, `email_mismatch` | 초대를 수락할 수 없습니다 |
| 403 | | 역할이 부족하거나, 멤버가 정지되었거나, 조직이 활성 상태가 아니거나, 워크스페이스가 보관되었거나, 이메일 도메인이 허용되지 않았습니다 |
| 404 | | 해당 조직, 워크스페이스, 멤버 또는 초대가 없거나, 내가 그 멤버가 아닙니다 |
| 409 | `name_taken` | 슬러그가 이미 사용 중입니다 |
| 409 | `already_a_member` | 그 사람이 이미 워크스페이스에 있습니다 |
| 409 | `owner_cannot_leave` | 소유자가 나가려고 했습니다 |
| 409 | `has_active_workspaces` | 조직에 아직 보관되지 않은 워크스페이스가 있습니다 |
| 429 | | 조직 생성, 참여 시도, CSV 내보내기 또는 초대가 너무 많습니다 |
| 503 | `billing_unavailable` | 이 인스턴스에서는 아직 사용량 보고서를 사용할 수 없습니다 |
| 503 | `audit_unavailable` | CSV 내보내기를 감사 로그에 기록하지 못했습니다. 다시 시도하세요. |

## 자주 묻는 질문

### 조직과 워크스페이스는 어떻게 다른가요?

조직은 학교나 팀이며, 소유자 한 명과 관리자, 멤버로 이루어집니다. 워크스페이스는 그 안에서 멤버들이 함께 작업하는 곳으로, 수업이나 프로젝트 팀 같은 단위입니다. 모든 워크스페이스는 하나의 조직에 속합니다.

### 워크스페이스 안에서 워크플로를 실행하려면 어떻게 하나요?

client.withWorkspace(workspaceId)로 그 워크스페이스용 클라이언트를 만들고, 그 클라이언트에서 run을 호출하세요. 원래 클라이언트는 계속 내 개인 공간에서 동작합니다.

### 새 조직이 pending 상태인 이유는 무엇인가요?

새 조직은 pending 상태로 시작하고, Nodaro가 승인하면 active가 됩니다. 그 전까지는 소유자만 조직을 볼 수 있으며, 조직 안의 어떤 것도 변경할 수 없습니다.

### 초대가 링크와 함께 반환되면 어떻게 해야 하나요?

초대를 보낸 사람에게 그 링크를 보여 주세요. status가 sent가 아닌 행은 이메일로 전송되지 않은 것입니다. 설치에 메일 공급자가 없거나 전송에 실패했기 때문입니다. 초대 자체는 존재하며, 그 링크가 초대에 접근하는 유일한 방법입니다.
