조직과 워크스페이스
TypeScript로 Nodaro 조직과 워크스페이스를 관리합니다. 학교와 팀을 만들고 멤버를 초대하며, 참여 코드를 발급하고 크레딧 사용량 보고서를 읽습니다.
Nodaro Cloud에서 사용할 수 있습니다
client.organizations는 학교나 팀인 조직을 멤버, 초대, 감사 로그, 사용량 보고서와 함께 관리합니다. client.workspaces는 그 안의 워크스페이스(수업이나 팀 프로젝트 등)를 멤버, 참여 코드와 함께 관리합니다. 이 메서드들은 워크스페이스 REST API를 호출합니다. 개념은 워크스페이스를 참조하세요.
조직은 Nodaro Cloud 기능이며, 인스턴스별로 켭니다. 조직이 없는 인스턴스에서는 이 페이지의 모든 메서드가 NotFoundError를 발생시킵니다.
소속과 동작
워크스페이스에 속하는 것과 워크스페이스에서 동작하는 것은 서로 다릅니다. 이 두 리소스는 누가 어디에 속하는지를 관리합니다. 워크스페이스에서 동작하려면, 즉 목록을 그 워크스페이스에서 읽고 새로 만든 결과물이 그곳에 저장되게 하려면, client.withWorkspace(workspaceId)로 클라이언트를 만들거나 createClient에 workspaceId를 전달하세요.
const classroom = client.withWorkspace(workspaceId)
await classroom.workflows.run(workflowId) // runs in the class
await client.workflows.run(workflowId) // runs in your personal spaceclient.me()는 내가 속한 조직과 워크스페이스, 그리고 lastWorkspaceId를 반환합니다.
모든 권한은 서버가 결정합니다. 호출자가 초대, 제거, 이름 변경을 할 수 있는지는 서버가 판단해 오류로 알려 줍니다. 설정에 따라 결과가 달라질 수 있으므로 내 클라이언트에서 짐작하지 마세요.
메서드
client.organizations
organizations.list()
내가 속한 조직을 나열합니다(GET /v1/orgs).
list(): Promise<{ data: OrganizationView[] }>const { data: orgs } = await client.organizations.list()organizations.get(id)
조직 하나를 읽습니다(GET /v1/orgs/:id).
get(id: string): Promise<{ data: OrganizationView }>속성
타입
const { data: org } = await client.organizations.get(orgId)
console.log(org.status) // pending, active, suspended or deletedorganizations.create(input)
나를 소유자로 하는 조직을 만듭니다(POST /v1/orgs). 새 조직은 보통 pending 상태로 시작하고, Nodaro가 승인하면 active가 됩니다. pending 상태인 동안에는 나만 조직을 볼 수 있고 조직 안의 어떤 것도 변경할 수 없으므로, 사용자에게 승인을 기다리는 중이라고 알려 주세요.
create(input: { name: string; kind: "school" | "team"; slug?: string; acceptTerms?: boolean; settings?: OrgSettings }): Promise<{ data: OrganizationView }>속성
타입
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).
update(id: string, input: { name?: string; settings?: OrgSettings }): Promise<{ data: OrganizationView }>속성
타입
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로 실패합니다.
delete(id: string): Promise<{ data: { id: string; status: string } }>속성
타입
await client.organizations.delete(orgId)organizations.transferOwnership(id, userId)
다른 멤버를 소유자로 지정합니다(POST /v1/orgs/:id/transfer-ownership). 나는 관리자가 됩니다.
transferOwnership(id: string, userId: string): Promise<{ data: { orgId: string; ownerUserId: string } }>속성
타입
await client.organizations.transferOwnership(orgId, newOwnerId)organizations.leave(id)
조직에서 나갑니다(POST /v1/orgs/:id/leave). 소유자는 나갈 수 없습니다. 먼저 소유권을 이전하세요. 그렇지 않으면 호출이 409 owner_cannot_leave로 실패합니다.
leave(id: string): Promise<{ data: { orgId: string; left: boolean } }>속성
타입
await client.organizations.leave(orgId)조직 멤버
조직의 멤버를 나열하고, 변경하고, 제거합니다(/v1/orgs/:id/members). 멤버는 active 또는 suspended 상태입니다. 정지된 멤버는 자리를 유지하지만 동작할 수 없습니다. 소유자는 정지하거나, 제거하거나, 강등할 수 없습니다.
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 } }>속성
타입
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). 주소마다 행 하나를 반환합니다.
invite(orgId: string, input: {
emails: string[]
orgRole?: "admin" | "member"
workspaceId?: string
workspaceRole?: "admin" | "member"
}): Promise<{ data: InvitationDelivery[] }>속성
타입
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가 반환됩니다.
초대 관리
초대를 나열하고, 취소하고, 다시 보냅니다.
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 } }>속성
타입
const { data: open } = await client.organizations.listInvitations(orgId, { status: "open" })
await client.organizations.resendInvitation(open[0].id)초대 미리 보기와 수락
previewInvitation()은 공개입니다. 초대받은 사람이 아직 로그인하지 않은 상태에서도 동작하며, 주소를 가려서 반환합니다. acceptInvitation()에는 초대의 이메일과 일치하는 이메일로 로그인한 호출자가 필요합니다.
previewInvitation(token: string): Promise<{ data: InvitationPreview }>
acceptInvitation(token: string): Promise<{ data: { orgId: string; workspaceId: string | null } }>속성
타입
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). 조직이 정지된 동안에도 계속 읽을 수 있습니다.
audit(orgId: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<OrgAuditEntry>>속성
타입
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 텍스트로 반환합니다.
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>속성
타입
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()에 담긴 목록과 같습니다.
list(): Promise<{ data: WorkspaceSummary[]; lastWorkspaceId: string | null }>const { data: workspaces, lastWorkspaceId } = await client.workspaces.list()workspaces.listForOrg(orgId, opts?)
조직 하나의 워크스페이스를 나열합니다(GET /v1/orgs/:id/workspaces).
listForOrg(orgId: string, opts?: { includeArchived?: boolean }): Promise<{ data: WorkspaceView[] }>속성
타입
const { data: classes } = await client.workspaces.listForOrg(orgId)workspaces.get(id)
워크스페이스 하나의 전체 정보를 읽습니다(GET /v1/workspaces/:id).
get(id: string): Promise<{ data: WorkspaceView }>속성
타입
const { data: workspace } = await client.workspaces.get(workspaceId)workspaces.create(orgId, input)
조직 안에 워크스페이스를 만듭니다(POST /v1/orgs/:id/workspaces). 조직이 active 상태여야 합니다.
create(orgId: string, input: { name: string; slug?: string; description?: string; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>속성
타입
const { data: classOne } = await client.workspaces.create(orgId, { name: "Class 1" })workspaces.update(id, input)
워크스페이스를 변경합니다(PATCH /v1/workspaces/:id).
update(id: string, input: { name?: string; description?: string | null; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>속성
타입
await client.workspaces.update(workspaceId, { settings: { members_can_create_projects: true } })workspaces.setArchived(id, archived)
워크스페이스를 보관하거나 복원합니다(POST /v1/workspaces/:id/archive 또는 /unarchive). 보관은 되돌릴 수 있으며 아무것도 파기하지 않습니다. 워크스페이스는 새 작업을 받지 않게 되지만, 모든 내용을 계속 읽을 수 있습니다. 보관된 워크스페이스에 쓰기를 시도하면 ForbiddenError로 실패합니다.
setArchived(id: string, archived: boolean): Promise<{ data: WorkspaceView }>속성
타입
await client.workspaces.setArchived(workspaceId, true)워크스페이스 멤버
워크스페이스의 멤버를 나열하고, 추가하고, 변경하고, 제거합니다(/v1/workspaces/:id/members). 추가하려면 그 사람이 이미 조직에 속해 있어야 합니다. 새로운 사람을 들이려면 초대하세요. 조직의 소유자와 관리자는 목록에 없어도 모든 워크스페이스의 관리자입니다.
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 } }>속성
타입
await client.workspaces.addMember(workspaceId, { userId, role: "member" })
await client.workspaces.updateMember(workspaceId, userId, { creditCap: 500 })조직의 활성 멤버가 아닌 사람을 추가하면 400 not_org_member로 실패하고, 같은 사람을 두 번 추가하면 409 already_a_member로 실패합니다.
참여 코드
참여 코드가 있으면 초대 없이 워크스페이스에 참여할 수 있습니다. 코드를 읽거나 변경할 수 있는 사람은 워크스페이스 관리자뿐입니다.
getJoinCode(id: string): Promise<{ data: JoinCodeView | null }>
actOnJoinCode(id: string, action: "rotate" | "enable" | "disable"): Promise<{ data: JoinCodeView }>속성
타입
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). 내 클라이언트가 어느 워크스페이스에서 동작하든 작동하므로, 오래된 선택 때문에 막히는 일이 없습니다.
join(code: string): Promise<{ data: { orgId: string; workspaceId: string } }>속성
타입
const { data: joined } = await client.workspaces.join("BCDFGHJK")알 수 없는 코드, 꺼진 코드, 보관된 워크스페이스의 코드는 모두 400 join_code_invalid로 실패합니다. 세 경우 모두 같은 응답이므로, 코드로 무엇이 존재하는지 알아낼 수 없습니다. 시도가 너무 많으면 RateLimitedError가 반환됩니다.
워크스페이스 사용량
조직과 같은 크레딧 보고서를 워크스페이스 하나에 대해 제공합니다(GET /v1/workspaces/:id/usage). 멤버는 자신의 실행을 보고, 관리자는 모든 사람의 실행을 보며 userId로 필터링할 수 있습니다.
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>속성
타입
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가 아닌 행은 이메일로 전송되지 않은 것입니다. 설치에 메일 공급자가 없거나 전송에 실패했기 때문입니다. 초대 자체는 존재하며, 그 링크가 초대에 접근하는 유일한 방법입니다.
관련 문서
워크스페이스
클라이언트
워크스페이스와 조직
워크플로와 프로젝트
최종 업데이트