# 클라이언트

> createClient로 Nodaro SDK 클라이언트를 만들고 기본 URL, 인증, 타임아웃, 워크스페이스를 설정하는 방법과 클라이언트가 제공하는 모든 리소스를 안내합니다.

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

**클라이언트**는 `createClient()`가 반환하는 객체입니다. `NodaroClient`는 내 기본 URL, 인증 공급자, 설정을 보관하고, `client.workflows`나 `client.nodes`처럼 Nodaro API의 모든 부분을 리소스로 제공합니다. 클라이언트는 한 번 만들고 모든 호출에서 다시 사용하세요.

## 클라이언트 만들기
```ts

const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
timeoutMs: 120_000,
})
```

```ts
createClient(options: ClientOptions): NodaroClient
```

<TypeTable
type={{
baseUrl: {
type: 'string',
required: true,
description: "Nodaro 서버입니다. 예를 들어 https://app.nodaro.ai나 셀프 호스팅 설치의 주소입니다. 브라우저 앱에서 같은 출처로 요청할 때는 빈 문자열을 사용하세요. 끝의 슬래시는 제거됩니다.",
},
auth: {
type: 'Auth',
required: true,
description: "인증 공급자입니다. new StaticTokenAuth(token), supabaseAuth(supabase), new CallbackAuth(fn), 또는 getToken() 메서드가 있는 모든 객체를 사용할 수 있습니다.",
typeDescriptionLink: '/docs/developers/sdk/auth',
},
fetch: {
type: 'typeof fetch',
default: 'globalThis.fetch',
description: "테스트, 재시도, 추적에 쓰는 사용자 지정 fetch 함수입니다.",
},
timeoutMs: {
type: 'number',
default: '60000',
description: "각 요청의 타임아웃이며, 단위는 밀리초입니다. 시간이 다 되면 요청이 중단됩니다.",
},
workspaceId: {
type: 'string',
description: "모든 요청이 동작하는 워크스페이스이며, X-Nodaro-Workspace 헤더로 전송됩니다. 생략하면 내 개인 공간에서 동작합니다. Nodaro Cloud 조직에서만 사용할 수 있습니다.",
},
clientLabel: {
type: 'string',
default: "'sdk/<version>'",
description: "X-Nodaro-Client 헤더의 값입니다. Nodaro는 이 값을 각 작업의 출처로 기록합니다. SDK 위에 다른 도구를 만들 때만 설정하세요.",
},
}}
/>

`NodaroClient`는 클래스로도 내보내지므로, 클라이언트를 받는 함수에 타입을 지정할 수 있습니다.

```ts

async function countWorkflows(client: NodaroClient, projectId: string) {
const { data } = await client.workflows.list({ projectId })
return data.length
}
```

## 클라이언트의 리소스
모든 리소스는 `createClient`가 만들며, `client.<resource>`로 접근합니다.

| 리소스 | 다루는 내용 | 참조 |
| --- | --- | --- |
| `client.workflows` | 워크플로: 만들기, 업데이트, 공유, 내보내기, 가져오기, 실행 | [워크플로와 프로젝트](https://nodaro.ai/docs/developers/sdk/workflows) |
| `client.projects` | 워크플로를 담는 프로젝트 | [워크플로와 프로젝트](https://nodaro.ai/docs/developers/sdk/workflows) |
| `client.sessions` | 프로젝트 안의 생성 세션 | [워크플로와 프로젝트](https://nodaro.ai/docs/developers/sdk/workflows#clientsessions) |
| `client.executions` | 워크플로 전체의 실행 | [작업과 실행](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) |
| `client.jobs` | 개별 생성 작업과 그 평가, 필터 | [작업과 실행](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) |
| `client.webhooks` | 작업이 끝날 때 Nodaro가 호출하는 엔드포인트 | [작업과 실행](https://nodaro.ai/docs/developers/sdk/jobs-and-executions#clientwebhooks) |
| `client.videoPro` | 동영상 생성 Pro 실행 중지 또는 이어 가기 | [작업과 실행](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) |
| `client.nodes` | 노드 카탈로그와 단일 노드 실행 | [노드 실행](https://nodaro.ai/docs/developers/sdk/nodes) |
| `client.apps` | 게시된 앱과 그 실행 | [앱과 템플릿](https://nodaro.ai/docs/developers/sdk/apps-and-templates) |
| `client.templates` | 템플릿 마켓플레이스 | [앱과 템플릿](https://nodaro.ai/docs/developers/sdk/apps-and-templates) |
| `client.tutorials` | 튜토리얼 동영상과 튜토리얼 워크플로 | [앱과 템플릿](https://nodaro.ai/docs/developers/sdk/apps-and-templates) |
| `client.llm` | 언어 모델의 구조화된 출력 | [LLM과 Reduce](https://nodaro.ai/docs/developers/sdk/llm-and-reduce) |
| `client.reduce` | 여러 결과 중 가장 좋은 것을 고르거나 하나로 합치기 | [LLM과 Reduce](https://nodaro.ai/docs/developers/sdk/llm-and-reduce) |
| `client.uploads` | 파일 업로드 | [미디어와 업로드](https://nodaro.ai/docs/developers/sdk/media-and-uploads) |
| `client.library` | 내가 저장한 미디어 | [미디어와 업로드](https://nodaro.ai/docs/developers/sdk/media-and-uploads) |
| `client.gallery` | 공개 갤러리와 내 즐겨찾기 | [미디어와 업로드](https://nodaro.ai/docs/developers/sdk/media-and-uploads#clientgallery) |
| `client.media` | 미디어 다운로드, 자르기, 자막, 오버레이, 콜라주 | [미디어와 업로드](https://nodaro.ai/docs/developers/sdk/media-and-uploads) |
| `client.voices` | 보이스, 보이스 체인저, 보이스 디자인, 더빙 | [보이스와 오디오](https://nodaro.ai/docs/developers/sdk/voices-and-audio) |
| `client.audio` | 오디오 분리, 추출, 믹스, 자르기, 텍스트 변환 | [보이스와 오디오](https://nodaro.ai/docs/developers/sdk/voices-and-audio) |
| `client.edit` | 무음 감지, 오디오 동기화, 편집 계획, EDL 렌더링 | [편집](https://nodaro.ai/docs/developers/sdk/editing) |
| `client.scene3d` | 편집 가능한 3D 장면과 3D 렌더링 Pro | [3D 장면](https://nodaro.ai/docs/developers/sdk/scenes-3d) |
| `client.characters` | 캐릭터 | [캐릭터](https://nodaro.ai/docs/developers/sdk/characters) |
| `client.locations` | 장소 | [장소](https://nodaro.ai/docs/developers/sdk/locations) |
| `client.objects` | 오브젝트와 소품 | [오브젝트와 크리처](https://nodaro.ai/docs/developers/sdk/objects-and-creatures) |
| `client.creatures` | 동물과 크리처 | [오브젝트와 크리처](https://nodaro.ai/docs/developers/sdk/objects-and-creatures) |
| `client.community` | 함께 쓰는 커뮤니티 에셋 라이브러리 | [커뮤니티 라이브러리](https://nodaro.ai/docs/developers/sdk/community) |
| `client.studio` | 스튜디오 프로덕션 | [스튜디오 프로덕션](https://nodaro.ai/docs/developers/sdk/studio) |
| `client.shots` | 공유 링크 뒤에 있는 공유 샷 기록 | [스튜디오 프로덕션](https://nodaro.ai/docs/developers/sdk/studio) |
| `client.recast` | Recast 실행과 작성된 스크립트 | [Recast](https://nodaro.ai/docs/developers/sdk/recast) |
| `client.pipelines` | 스토리를 동영상으로 만드는 파이프라인 | [파이프라인](https://nodaro.ai/docs/developers/sdk/pipelines) |
| `client.copilot` | Copilot 스레드, Nodaro 앱 안에서만 사용 | [Copilot](https://nodaro.ai/docs/developers/sdk/copilot) |
| `client.models` | 모델 카탈로그 | [모델과 크레딧](https://nodaro.ai/docs/developers/sdk/models-and-credits) |
| `client.credits` | 내 잔액과 모델 가격 | [모델과 크레딧](https://nodaro.ai/docs/developers/sdk/models-and-credits) |
| `client.billing` | 요금제, 가격, 내 구독 | [모델과 크레딧](https://nodaro.ai/docs/developers/sdk/models-and-credits#clientbilling) |
| `client.pickerCatalogs` | 각 피커에서 쓸 수 있는 옵션 | [피커, 프리셋, 프롬프트](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.catalogs` | 모든 피커 카탈로그를 한 번의 호출로 | [피커, 프리셋, 프롬프트](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.presets` | 저장된 노드 프리셋과 기본 제공 노드 프리셋 | [피커, 프리셋, 프롬프트](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.palettes` | 내 컬러 팔레트 | [피커, 프리셋, 프롬프트](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts#clientpalettes) |
| `client.moodboards` | 내 무드보드 | [피커, 프리셋, 프롬프트](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts#clientmoodboards) |
| `client.promptHelper` | 프롬프트 마법사 | [피커, 프리셋, 프롬프트](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.organizations` | 조직, 멤버, 초대 | [조직과 워크스페이스](https://nodaro.ai/docs/developers/sdk/organizations) |
| `client.workspaces` | 워크스페이스, 멤버, 참여 코드 | [조직과 워크스페이스](https://nodaro.ai/docs/developers/sdk/organizations) |
| `client.developerApps` | 내가 소유한 OAuth 앱 | [OAuth와 개발자 앱](https://nodaro.ai/docs/developers/sdk/developer-apps) |
| `client.oauth` | 코드 교환, 토큰 폐기, 동의 화면 데이터 | [OAuth와 개발자 앱](https://nodaro.ai/docs/developers/sdk/developer-apps) |

클라이언트 자체에는 메서드가 5개 더 있습니다. [`me()`](#me), [`connectedApps()`](#connectedapps), [`revokeConnectedApp()`](#revokeconnectedappauthorizationid), [`withWorkspace()`](#withworkspaceworkspaceid), [`request()`](#requestmethod-path-options)입니다.

## 메서드가 반환하는 값
- **엔벨로프는 그대로 유지됩니다.** 엔드포인트가 `{ "data": ... }`로 응답하면 메서드는 그 엔벨로프로 이행하므로, `const { data } = await client.workflows.get(id)`처럼 작성합니다. 페이지 단위 목록에는 `nextCursor` 같은 커서가 `data` 옆에 추가됩니다.
- **일부 리소스는 페이로드를 반환합니다.** 몇몇 메서드는 응답을 풀어서 반환합니다. 예를 들어 `client.characters.list()`는 `{ characters, nextCursor }`로, `client.credits.balance()`는 잔액 자체로 이행합니다. 정확한 반환 타입은 각 참조 페이지에 나와 있습니다.
- **삭제와 취소**는 보통 `{ success: true }`로 이행합니다.
- **필드 이름은 전송 형식을 따릅니다.** `Job`은 API가 그렇게 보내므로 `output_data`나 `created_at` 같은 snake_case 필드를 사용합니다. `Workflow`와 `WorkflowExecution`은 camelCase를 사용합니다.

모든 응답 타입과 입력 타입이 내보내지므로 `import type`으로 가져올 수 있습니다. [타입](https://nodaro.ai/docs/developers/sdk/types)을 참조하세요.

## me()
```ts
me(): Promise<UserIdentity & MeOrganizations>
```

현재 토큰 뒤에 있는 사용자 정보를 반환합니다(`GET /v1/me`). 유효한 토큰이면 API 토큰이든 OAuth 액세스 토큰이든 브라우저 세션이든 그 소유자로 이행합니다. 토큰이 없거나 유효하지 않으면 `UnauthorizedError`가 발생합니다.

```ts
const me = await client.me()
console.log(me.email, me.tier)
```

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `id` | `string` | Nodaro 사용자 ID입니다. |
| `email` | `string` | 사용자의 이메일 주소입니다. |
| `displayName` | `string \| null` | 표시 이름이며, 설정되지 않았으면 `null`입니다. |
| `avatarUrl` | `string \| null` | 아바타 URL이며, 설정되지 않았으면 `null`입니다. |
| `tier` | `string` | 저장된 구독 등급이며, `"free"`나 `"pro"` 같은 값입니다. 종량제를 포함해 실제로 적용되는 등급은 [`client.credits.balance()`](https://nodaro.ai/docs/developers/sdk/models-and-credits)의 `effectiveTier`에서 읽으세요. |
| `isAdmin` | `boolean` | 사용자가 관리자인지 여부입니다. 무엇을 보여 줄지 정할 때만 사용하세요. 모든 권한은 서버가 직접 확인합니다. |

조직이 있는 Nodaro Cloud 인스턴스에서는 결과에 `organizations`, `workspaces`, `lastWorkspaceId`, `organizationsUnavailable`도 포함됩니다. 세 가지 상태를 각각 다르게 처리하세요.

| 보이는 것 | 의미 | 할 일 |
| --- | --- | --- |
| 필드가 없음 | 인스턴스에 조직이 없음 | 워크스페이스 전환기를 표시하지 마세요. |
| 필드가 있지만 비어 있음 | 계정이 어떤 조직에도 속하지 않음 | 조직을 만들거나 참여하도록 안내하세요. |
| `organizationsUnavailable: true` | 조회에 실패함 | 이미 선택된 항목을 유지하세요. 사용자에게 접근 권한을 잃었다고 알리지 마세요. |

## connectedApps()
```ts
connectedApps(): Promise<ConnectedApp[]>
```

로그인한 사용자의 계정에 접근할 수 있는 모든 앱을 최근 권한 부여 순으로 반환합니다(`GET /v1/me/connected-apps`). 목록에는 개발자 앱, AI 어시스턴트의 MCP 커넥터, 셀프 호스팅 설치가 포함됩니다. **연결된 앱** 설정 화면의 데이터입니다. 권한 부여가 무엇인지는 [OAuth](https://nodaro.ai/docs/developers/oauth)를 참조하세요.

**이 메서드에는 로그인한 브라우저 세션이 필요합니다.** [`supabaseAuth()`](https://nodaro.ai/docs/developers/sdk/auth#supabaseauthsupabase)나 `createSharedSupabaseClient`의 세션 토큰을 받습니다. API 토큰(`ndr_…`)이나 앱 토큰(`ndr_app_…`)은 `UnauthorizedError`로 거부하므로, 앱은 자신이 쓰는 권한 부여를 나열하거나 취소할 수 없습니다. 서버 측 통합에서는 호출할 수 없습니다.

```ts
const apps = await client.connectedApps()
// [{ authorizationId, name, kind, homepageUrl, scopes, connectedAt, lastUsedAt }]
```

| 필드 | 타입 | 설명 |
| --- | --- | --- |
| `authorizationId` | `string` | 권한 부여입니다. `revokeConnectedApp()`에 전달하세요. |
| `name` | `string \| null` | 앱의 이름입니다. 스스로 등록한 MCP 클라이언트는 이름을 직접 정했습니다. |
| `kind` | `DeveloperAppKind` | `user`(개발자 앱), `first_party_mcp`, `dynamic_mcp`(스스로 등록한 MCP 클라이언트), `community_instance`(셀프 호스팅 설치) 중 하나입니다. |
| `homepageUrl` | `string \| null` | 앱이 제공한 경우 앱의 홈페이지입니다. |
| `scopes` | `DeveloperAppScope[]` | 권한 부여가 가진 스코프입니다. |
| `connectedAt` | `string` | 사용자가 접근을 허용한 시각으로, ISO 8601 형식입니다. |
| `lastUsedAt` | `string \| null` | 권한 부여로 발급된 토큰이 가장 최근에 쓰인 시각이며, 한 번도 쓰이지 않았으면 `null`입니다. |

## revokeConnectedApp(authorizationId)
```ts
revokeConnectedApp(authorizationId: string): Promise<void>
```

앱의 접근을 끝냅니다(`POST /v1/me/connected-apps/:authorizationId/revoke`). 권한 부여와 그로 발급된 모든 토큰을 즉시 취소하므로, 앱의 다음 호출은 401로 실패합니다. `connectedApps()`처럼 로그인한 브라우저 세션이 필요합니다. 권한 부여가 사용자의 것이 아니거나 이미 취소되었으면 `NotFoundError`를 발생시킵니다.

```ts
await client.revokeConnectedApp(app.authorizationId)
```

## withWorkspace(workspaceId)
```ts
withWorkspace(workspaceId: string | null): NodaroClient
```

`workspaceId`에서 동작하는 **새** 클라이언트를 반환합니다. 새 클라이언트는 원래 클라이언트의 인증, 기본 URL, 타임아웃, fetch를 공유합니다. 내 개인 공간을 쓰려면 `null`을 전달하세요.

```ts
const classroom = client.withWorkspace(workspaceId)

await classroom.workflows.run(workflowId) // runs in the workspace
await client.workflows.run(workflowId)    // runs in the personal space
```

이 메서드는 현재 클라이언트를 바꾸지 않고 새 클라이언트를 반환합니다. 따라서 한 클라이언트에서 동시에 실행되는 두 작업의 워크스페이스가 서로 섞일 일이 없습니다.

워크스페이스는 **범위**를 정할 뿐, **접근 권한**은 정하지 않습니다. 목록을 어느 워크스페이스에서 읽을지, 새 항목을 어디에 만들지를 정합니다. ID로 지정한 항목을 읽고, 바꾸고, 삭제하고, 실행할 수 있는지는 그 항목이 속한 워크스페이스에 따라 정해집니다. 워크스페이스를 빠뜨려도 내 작업이 숨겨지지 않고, 잘못 지정해도 다른 사람의 작업에 닿지 않습니다.

워크스페이스는 Nodaro Cloud의 조직에 속합니다. [조직과 워크스페이스](https://nodaro.ai/docs/developers/sdk/organizations)와 [워크스페이스](https://nodaro.ai/docs/concepts/workspaces)를 참조하세요.

## request(method, path, options)
```ts
request<T>(method: string, path: string, options?: {
body?: unknown
query?: Record<string, string | number | boolean | undefined>
headers?: Record<string, string>
signal?: AbortSignal
}): Promise<T>
```

모든 엔드포인트에 요청을 보냅니다. 아직 리소스 메서드가 없는 몇몇 엔드포인트에 사용하세요. 인증 헤더와 워크스페이스를 추가하고, `body`를 JSON으로 보내고, `timeoutMs`를 적용하며, 리소스 메서드와 같은 [타입 지정 오류](https://nodaro.ai/docs/developers/sdk/errors)를 발생시킵니다.

```ts
// The same request that client.jobs.list() sends
const page = await client.request<{ data: unknown[]; next: string | null }>("GET", "/v1/jobs", {
query: { type: "llm-structured", limit: 20 },
})
```

`FormData` 본문은 멀티파트 업로드로 전송됩니다. 값이 `undefined`인 쿼리 항목은 빠집니다. 모든 엔드포인트와 필드는 [REST API 참조](https://nodaro.ai/docs/developers/api)에 나와 있습니다.

## 타임아웃과 사용자 지정 fetch
`timeoutMs`는 제한 시간보다 오래 걸리는 요청을 중단하며, 기본값은 60초입니다. 대부분의 생성은 적당한 HTTP 타임아웃보다 오래 걸리므로, 생성을 시작한 다음 작업을 폴링하세요. [`client.nodes.runAndWait()`](https://nodaro.ai/docs/developers/sdk/nodes)가 두 가지를 모두 처리합니다. 스트리밍 메서드인 `client.copilot.stream()`과 `client.media.downloadVideoProgress()`는 몇 분 동안 열려 있도록 만들어졌으므로 타임아웃을 적용하지 않습니다.

요청이 전달되는 방식을 바꾸려면 직접 만든 `fetch`를 전달하세요.

- **테스트.** 목에서 미리 준비한 `Response` 객체를 반환합니다.
- **재시도.** 전역 `fetch`를 5xx 응답에서 재시도하는 헬퍼로 감쌉니다.
- **추적.** 사용하는 추적 또는 모니터링 라이브러리로 감쌉니다.

```ts
const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
fetch: (input, init) => tracedFetch(input, init),
})
```

## 런타임과 브라우저
SDK는 `fetch`와 `URL`만 사용합니다. 둘 다 Node.js 20 이상, 최신 브라우저, React Native, Cloudflare Workers, Deno, Bun에서 전역으로 제공됩니다. 폴리필은 필요하지 않습니다.

- **OAuth 토큰과 CORS.** OAuth 액세스 토큰으로 Nodaro를 호출하는 브라우저 앱은 개발자 앱의 `allowedOrigins`에 등록된 출처에서 실행되어야 합니다. [OAuth와 개발자 앱](https://nodaro.ai/docs/developers/sdk/developer-apps)을 참조하세요.
- **세션과 CORS.** `supabaseAuth`를 사용하는 브라우저 앱은 이 목록으로 확인하지 않습니다.
- **클라이언트 레이블.** 서버에서는 SDK가 `X-Nodaro-Client: sdk/<version>`을 보내고, Nodaro는 이 값을 각 작업의 출처로 기록합니다. 브라우저에서는 브라우저의 `Origin` 헤더가 이미 내 앱을 알려 주므로 기본 레이블을 보내지 않습니다. 직접 설정한 `clientLabel`은 항상 전송됩니다.

## 자주 묻는 질문

### Nodaro SDK에서는 어떤 기본 URL을 사용해야 하나요?

Nodaro Cloud에서는 https://app.nodaro.ai를 사용하고, 셀프 호스팅 설치에서는 내 인스턴스의 주소를 사용하세요. Nodaro와 같은 출처에서 제공되는 브라우저 앱에서는 빈 문자열을 사용하세요.

### 기본 요청 타임아웃은 얼마인가요?

60초입니다. timeoutMs 옵션으로 바꿀 수 있습니다. 생성은 어떤 요청보다도 오래 걸리므로, 타임아웃을 늘리는 대신 생성을 시작한 다음 작업을 폴링하세요.

### 워크스페이스에서 요청을 보내려면 어떻게 하나요?

client.withWorkspace(workspaceId)를 호출하세요. 모든 요청을 그 워크스페이스에서 보내는 새 클라이언트가 반환되며, 원래 클라이언트는 계속 내 개인 공간에서 동작합니다.

### SDK 메서드가 없는 엔드포인트는 어떻게 호출하나요?

client.request(method, path, options)를 사용하세요. 다른 모든 리소스 메서드와 같은 인증 헤더를 보내고, 같은 타임아웃을 적용하며, 같은 타입 지정 오류를 발생시킵니다.
