# 작업

> Nodaro 작업의 상태와 결과를 폴링하는 방법입니다. 작업 목록 필터링, 결과 평가와 즐겨찾기, 이미지 크기 읽기, 작업 100개 일괄 확인, 작업 취소도 다룹니다.

Source: https://nodaro.ai/ko/docs/developers/api/jobs

**작업**은 Nodaro에서 생성의 한 단위입니다. 이미지 1장, 동영상 렌더링 1회, 음성 클립 1개가 각각 하나의 작업입니다. 노드를 실행하면 작업 ID가 반환되고, 워크플로를 실행하면 AI 노드마다 작업이 하나씩 만들어집니다. 작업 엔드포인트는 각 작업의 상태, 진행률, 결과, 크레딧을 알려 줍니다. 작업이 `completed`, `failed`, `cancelled` 중 하나가 될 때까지 폴링한 다음, `output_data`에서 결과를 읽으세요.

## 엔드포인트
| 메서드 | 경로 | 기능 |
| --- | --- | --- |
| `GET` | `/v1/jobs/:id/status` | 폴링용 간단한 상태입니다. 상태, 진행률, 결과, 오류를 반환합니다. |
| `GET` | `/v1/jobs/:id` | 보낸 내용(`input_data`)과 크레딧을 포함한 작업 전체입니다. |
| `GET` | `/v1/jobs` | 내 작업을 최신순으로, 페이지 단위로 반환합니다. |
| `GET` | `/v1/jobs/status?ids=…` | 작업 최대 100개의 상태입니다. ID는 쿼리에 넣습니다. |
| `POST` | `/v1/jobs/batch-status` | 작업 최대 100개의 상태입니다. ID는 본문에 넣습니다. |
| `PUT` | `/v1/jobs/:id/rating` | 내 작업 하나를 `good` 또는 `bad`로 평가하거나, 평가를 지웁니다. |
| `POST` | `/v1/gallery/favorite` | 작업을 즐겨찾기에 추가하거나 즐겨찾기에서 제거합니다. |
| `GET` | `/v1/gallery/favorites` | 내가 즐겨찾기한 작업 ID입니다. |
| `POST` | `/v1/jobs/:id/cancel` | 작업을 취소하고 예약된 크레딧을 해제합니다. |
| `DELETE` | `/v1/jobs/:id` | 작업과 작업이 만든 비공개 미디어를 삭제합니다. |
| `GET` | `/v1/component/execute/:jobId/wait-limit` | 서버가 컴포넌트 실행을 얼마나 기다리는지 반환합니다. |
| `POST` | `/v1/generate-video-pro/:jobId/stop` | 동영상 생성 Pro 실행을 중지하고 완성된 세그먼트를 유지합니다. |
| `POST` | `/v1/generate-video-pro/continue` | 동영상 생성 Pro 실행을 새 작업으로 이어 갑니다. |
| `POST` | `/v1/credits/video-pro-estimate` | 동영상 생성 Pro 실행을 시작하지 않고 가격을 계산합니다. |

OAuth 토큰으로 작업을 읽으려면 `jobs:read` 스코프가 필요합니다. 개인 API 토큰에는 스코프가 필요하지 않습니다. 폴링하는 대신 작업이 끝날 때 Nodaro가 서버에 알려 주게 하려면 [Webhook 엔드포인트](https://nodaro.ai/docs/developers/api/webhooks#job-events)를 등록하세요.

## 작업 읽기
**curl**

```bash
curl -s https://app.nodaro.ai/v1/jobs/0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10/status \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

```json
{
"data": {
"id": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10",
"status": "completed",
"progress": 100,
"output_data": { "imageUrl": "https://…/0f1a9c2e.png" },
"error_message": null,
"error_hint": null,
"credit_status": "committed"
}
}
```

**TypeScript SDK**

```ts
const { data } = await client.jobs.getStatus(jobId)
if (data.status === 'completed') console.log(data.output_data)

// The full record, with input_data and credits:
const { data: job } = await client.jobs.get(jobId)
```

**CLI**

```bash
nodaro jobs get 0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10 --json
```

`GET /v1/jobs/:id/status`는 폴링 루프용으로 만들어졌습니다. `input_data`와 비용 필드를 생략하므로 `GET /v1/jobs/:id`보다 가볍습니다. 두 경로 모두 `{ "data": … }`로 응답하며, 존재하지 않거나 내 것이 아닌 작업에는 `404`로 응답합니다. 작업 필드는 서버가 보내는 그대로 snake_case를 사용합니다.

| 필드 | 의미 |
| --- | --- |
| `id` | 작업 ID입니다. |
| `status` | 작업이 현재 어느 단계에 있는지 나타냅니다. [작업 상태](#job-statuses)를 참고하세요. |
| `progress` | 0~100입니다. |
| `output_data` | 결과입니다. 미디어 작업에는 `imageUrl`, `videoUrl` 또는 `audioUrl`이 있고, `thumbnailUrl`이 함께 있는 경우가 많습니다. 다른 작업에는 `text`나 `json` 같은 고유 필드가 있습니다. |
| `error_message` | 작업이 실패한 이유를 문장으로 설명합니다. |
| `error_hint` | 두 종류의 실패에 대한 구조화된 이유입니다. [작업이 실패한 이유](#why-a-job-failed)를 참고하세요. |
| `credit_status` | `reserved`, `committed`, `refunded` 또는 `null`입니다. [작업의 크레딧](#credits-of-a-job)을 참고하세요. |
| `recovering` | 모델이 결과를 전달한 뒤 워커가 멈춘 작업을 플랫폼이 복구하는 동안 `true`입니다. |
| `input_data` | 전체 작업에만 있습니다. 보정을 거친 뒤 보낸 내용입니다. 예를 들어 최종 `prompt`, 내가 직접 보낸 `userPrompt`, `direction` ID가 들어 있습니다. |
| `credits` | 전체 작업과 목록에 있습니다. 작업이 접수될 때 예약된 크레딧입니다. 이 금액은 바뀌지 않습니다. |
| `credits_charged` | 전체 작업과 목록에 있습니다. 예약이 정산된 뒤 작업에 든 비용 또는 `null`입니다. [작업의 크레딧](#credits-of-a-job)을 참고하세요. |
| `job_type`, `source`, `source_detail` | 전체 작업에만 있습니다. 작업의 종류와 출처입니다. `source`는 `internal`, `mcp`, `app`, `cli`, `sdk`, `extension`, `web`, `api` 중 하나이고, `source_detail`은 `sdk/1.10.0`처럼 클라이언트 이름을 나타냅니다. |
| `session_id` | 전체 작업과 목록에 있습니다. 작업이 정리된 [생성 세션](https://nodaro.ai/docs/developers/api/nodes#sessions) 또는 `null`입니다. |
| `favorited`, `rating` | 전체 작업과 목록에 있습니다. 내 즐겨찾기(`true` 또는 `false`)와 내 평가(`"good"`, `"bad"` 또는 `null`)입니다. [즐겨찾기와 평가](#favorites-and-ratings)를 참고하세요. |
| `created_at`, `started_at` | 전체 작업에만 있습니다. 타임스탬프입니다. |
| `completed_at` | 서버 시계 기준으로 작업이 끝난 시각이며, 끝나기 전에는 `null`입니다. 상태 경로도 이 값을 반환합니다. |

`input_data`와 `output_data`는 공개용 보기입니다. 서버 내부에서만 쓰는 필드는 모든 호출자에게서 제거됩니다.

[EDL 적용](https://nodaro.ai/docs/nodes/video/apply-edl) 작업의 `output_data`는 렌더가 무엇에서 잘렸는지 알려 줍니다. `quality`는 미리 보기면 `"proxy"`, 최종본이면 `"final"`이고, 요청이 계획 클립을 보냈으면 `clipKey`가 그 클립을 가리킵니다. 오래된 EDL 적용 작업은 주문할 때의 품질로 읽힙니다.

### 이미지 작업의 이미지 크기
`output_data`에 `imageUrl`이 있는 모든 작업에는 이미지의 픽셀 크기도 담깁니다. 생성, 편집, 이미지 대 이미지 실행, 콜라주, 오버레이, 추출한 프레임 등이 해당합니다. 클라이언트는 이미지가 로드되기 전에 그리드를 배치하거나, 크기를 보여 주고 크기로 정렬할 수 있습니다.

| 필드 | 타입 | 의미 |
| --- | --- | --- |
| `width`, `height` | integer | 저장된 파일 기준 `imageUrl`의 픽셀 크기입니다. 다운로드하는 그대로이며, 무료 등급의 워터마크도 포함됩니다. EXIF 방향이 적용되므로 브라우저가 표시하는 크기입니다. |
| `images` | `{ url, width, height }[]` | 이미지를 여러 장 만든 작업에서 `imageUrls` 옆에 있습니다. 이미지마다 항목이 하나씩 `imageUrls` 순서로 들어갑니다. 크기를 읽을 수 없었던 항목에는 `url`만 있습니다. `imageUrls` 자체는 계속 문자열 배열입니다. |

```json
{
"imageUrl": "https://cdn.nodaro.ai/images/<jobId>.png",
"imageUrls": ["https://cdn.nodaro.ai/images/<jobId>.png", "https://cdn.nodaro.ai/images/<jobId>-v1.png"],
"width": 1024,
"height": 1536,
"images": [
{ "url": "https://cdn.nodaro.ai/images/<jobId>.png", "width": 1024, "height": 1536 },
{ "url": "https://cdn.nodaro.ai/images/<jobId>-v1.png", "width": 1024, "height": 1536 }
]
}
```

세 필드는 모두 선택 사항으로 다루세요. 크기가 기록되기 전에 완료된 작업에는 이 필드가 하나도 없으며, 서버가 읽을 수 없었던 크기는 추측하지 않고 빠집니다. 이 필드는 모든 작업 읽기에 있습니다: `GET /v1/jobs`, `GET /v1/jobs/:id`, `GET /v1/jobs/:id/status`. MCP 작업 도구인 `get_job`, `wait_for_job`, `get_asset`은 `outputUrl` 옆에 `width`와 `height`를 담습니다.

## 작업 상태
| 상태 | 최종 | 의미 |
| --- | --- | --- |
| `pending`, `queued`, `processing` | 아니요 | 작업이 실행을 기다리고 있거나 실행 중입니다. |
| `pending_review` | 아니요 | 결과가 나왔고, 배포 환경이 사람의 검토를 위해 보류하고 있습니다. 계속 폴링하세요. |
| `completed` | 예 | 결과가 `output_data`에 있습니다. |
| `failed` | 예 | `error_message`와 `error_hint`가 이유를 알려 줍니다. |
| `cancelled` | 예 | 내가 또는 플랫폼이 작업을 취소했습니다. |

**복구 중인 작업.** 모델이 이미 결과를 전달한 뒤 워커가 멈추면, 플랫폼이 복구하는 동안 작업은 `recovering: true`와 함께 `processing` 상태로 남습니다. 그 뒤 작업은 스스로 완료되거나 환불됩니다. 느린 모델에서는 수십 분이 걸릴 수 있으며, 이는 SDK의 기본 대기 시간보다 깁니다. `runAndWait`의 `JobTimeoutError`는 작업을 취소하지 않으므로, 나중에 작업을 다시 가져오세요.

**검토를 위해 보류된 작업.** 검토 정책을 등록한 배포 환경에서는 작업이 `pending_review` 상태가 될 수 있습니다. 작업 자체는 끝났고, 보류되는 동안 크레딧은 계속 `reserved` 상태이며, 결과를 공개할지는 사람이 결정합니다. 그 뒤 작업은 `completed`(승인), `policy-block` 힌트가 있는 `failed`(거부) 또는 `cancelled`로 끝납니다. 보류된 작업은 진행 중인 다른 작업처럼 취소할 수 있습니다. 요청을 다시 실행하지 마세요. 중복 요청도 보류되기 때문입니다. 배포 환경이 검토 기한을 정한 경우, 기한 안에 아무도 검토하지 않은 작업은 거부됩니다. 이 작업은 `failed`로 끝나고, 예약은 환불되며, 보류된 결과는 삭제됩니다. 보류된 작업이 자동으로 승인되는 일은 없습니다. SDK의 `runAndWait`는 `pending_review`를 처음 확인한 폴링에서 `JobHeldError`를 던지고, CLI의 `--watch`는 코드 `3`으로 종료됩니다.

## 효율적인 폴링
- **2~5초마다 폴링하세요.** 작업 상태는 밀리초가 아니라 초 단위로 바뀝니다.
- **상태 조회는 토큰의 요청 한도에 포함되지 않습니다.** 개인 API 토큰의 분당 한도에는 워크플로 실행과 워크플로 목록 조회만 포함됩니다. [요청 한도](https://nodaro.ai/docs/developers/api/rate-limits)를 참고하세요.
- **여러 작업을 한 번의 호출로 추적하세요.** 작업마다 요청을 하나씩 보내지 말고 [일괄 엔드포인트](#poll-many-jobs-at-once)를 사용하세요.
- **클라이언트가 대신 기다리게 하세요.** SDK의 `client.nodes.runAndWait`는 최대 15분 동안 2초마다 폴링하고, CLI의 `--watch`는 작업이 끝날 때까지 폴링합니다.

## 작업이 실패한 이유
[오류](https://nodaro.ai/docs/developers/api/errors)의 오류 표는 작업을 만들지 못한 요청을 다룹니다. 나중에 실패한 작업에는 `error_message`가 있고, 두 종류의 실패에는 구조화된 `error_hint`도 함께 있습니다.

**모델의 안전 필터가 요청을 차단한 경우:**

```json
{ "kind": "safety-block", "class": "safety", "retried": true, "suggestedProvider": "nano-banana-pro" }
```

- `class`는 `copyright`, `likeness`, `safety` 중 하나입니다. `copyright`나 `likeness` 차단은 최종 결과입니다. 같은 요청은 절대 통과하지 않습니다.
- 일부 모델에서는 `safety` 필터의 판정이 항상 일정하지는 않습니다. [GPT Image 2](https://nodaro.ai/docs/models/image/gpt-image-2), [GPT Image 2.5 Flare](https://nodaro.ai/docs/models/image/gpt-image-2-5-flare), [GPT Image 2.5 Sunburst](https://nodaro.ai/docs/models/image/gpt-image-2-5-sunburst)에서는 Nodaro가 같은 요청을 추가 비용 없이 한 번 다시 시도합니다. `retried`는 그 재시도가 이미 실행되었는지 나타냅니다.
- `suggestedProvider`는 모델에 권장 대체 모델이 있을 때만 나타납니다. 실제 모델 ID이므로, 같은 프롬프트와 레퍼런스를 이 모델로 보내세요.

**배포 환경의 정책이 작업을 거부한 경우:**

```json
{ "kind": "policy-block", "policyId": "brand-safety", "reason": "This image was not approved for release.", "hookPoint": "result" }
```

- `reason`은 배포 환경의 정책이 내 사용자를 위해 쓴 텍스트입니다. 그대로 보여 주세요.
- `hookPoint`는 작업이 실행되기 전에 거부되었으면 `request`이고, 실행 후 결과가 거부되었으면 `result`입니다. 검토자가 거부한 경우도 `result`입니다.
- 플랫폼은 정책 거부를 재시도하지 않으며 다른 모델을 제안하지 않습니다.

**그 밖의 실패에는 `error_hint`가 없습니다.** 설정이나 입력 미디어가 해당 모델에 유효하지 않아 모델이 요청 자체를 거부하면, `error_message`가 그렇게 알려 줍니다. 다시 실행하기 전에 설정이나 미디어를 바꾸세요. 모델 쪽 오류는 문구와 관계없이 다시 시도할 만합니다. `error_hint`는 `error_message`가 있는 모든 작업 페이로드에 나타납니다. 전체 작업, 상태 경로, 목록, 두 일괄 경로 모두 해당됩니다.

## 작업의 크레딧
`credit_status`는 작업의 크레딧 예약 상태를 따릅니다.

| 값 | 의미 |
| --- | --- |
| `reserved` | 작업이 실행되는 동안 크레딧이 예약됩니다. |
| `committed` | 작업이 결과를 전달했고 요금이 청구되었습니다. |
| `refunded` | 예약이 해제되었습니다. 예를 들어 안전 차단 뒤에 해제됩니다. |
| `null` | 작업에 보고할 크레딧 기록이 없습니다. |

이 필드는 `GET /v1/jobs`, `GET /v1/jobs/:id`, `GET /v1/jobs/:id/status`, `GET /v1/jobs/status`에 나타나며, `POST /v1/jobs/batch-status`에는 나타나지 않습니다. 안전 차단이나 정책 차단으로 끝난 생성은 항상 환불됩니다. 드문 예외는 정책이 결과를 거부하기 전에 크레딧이 이미 정산된 작업입니다. [크레딧](https://nodaro.ai/docs/developers/api/credits)을 참고하세요.

작업의 `credits`는 작업이 접수될 때 **예약한** 금액이며, 바뀌지 않습니다. `GET /v1/jobs`와 `GET /v1/jobs/:id`에는 `credits_charged`도 있습니다. 예약이 정산된 뒤 작업에 든 비용이며, 같은 크레딧 기록에서 읽습니다.

- `credits_charged`는 `credits`보다 적을 수 있습니다. 일부 작업은 다시 렌더링할 가능성이나 실행 후에 측정되는 길이를 감안해 실제 사용량보다 많이 예약합니다. 나머지는 잔액으로 돌아갑니다.
- 예약이 환불되면 `credits_charged`는 `0`입니다.
- 작업이 아직 실행 중이거나(`credit_status: "reserved"`), 작업이 자체적으로 아무것도 예약하지 않았으면 `credits_charged`는 `null`입니다.

작업 비용의 합계를 내려면 `credits`가 아니라 `credits_charged`를 합산하세요. 예를 들어 888크레딧을 예약하고 444크레딧으로 정산된 작업은 `credits: 888`, `credits_charged: 444`, `credit_status: "committed"`로 표시됩니다.

## 내 작업 목록
`GET /v1/jobs`는 내 작업을 최신순으로 `{ data: Job[], next }` 형식으로 반환합니다.

| 쿼리 | 의미 |
| --- | --- |
| `limit` | 페이지 크기이며, 최대 100입니다. |
| `cursor` | 이전 페이지의 `next` 값입니다. |
| `status` | 작업 상태 하나 이상: `pending`, `queued`, `processing`, `pending_review`, `completed`, `failed`, `cancelled`. 알 수 없는 상태는 `400 validation_error`로 응답합니다. |
| `type` | 작업을 만든 경로 하나 이상이며, `llm-structured`나 `video-analysis` 같은 값을 최대 20개까지 받습니다. |
| `origin` | 작업을 보낸 클라이언트 앱입니다. `studio`처럼 정확히 일치해야 합니다. |
| `favorited` | `true`는 내가 즐겨찾기한 작업을, `false`는 즐겨찾기하지 않은 작업을 남깁니다. |
| `rating` | `good`, `bad`, `rated`(평가가 있는 작업), `unrated`. |
| `batchId` | **이미지 생성** 일괄 처리 하나의 작업입니다. [한 번의 호출로 이미지 여러 장](https://nodaro.ai/docs/developers/api/nodes#several-images)을 참고하세요. |
| `attachToCharacterId` | 한 캐릭터의 작업입니다. [캐릭터](https://nodaro.ai/docs/developers/api/characters)를 참고하세요. |

`status`와 `type`은 쉼표로 구분한 목록(`status=failed,cancelled`)이나 반복한 매개변수(`type=a&type=b`)를 받습니다. 모든 필터는 정확히 일치해야 하며, 필터끼리 함께 쓸 수 있습니다. 한 페이지에 `limit`보다 적은 행이 있거나 행이 하나도 없어도 `next`가 있을 수 있습니다. 행 수를 세지 말고, `next`가 있는 동안 계속 다음 페이지를 가져오세요.

```ts
const { data: runs, next } = await client.jobs.list({ type: 'llm-structured', origin: 'my-app' })
```

## 즐겨찾기와 평가
내 생성 결과에는 두 가지 표시를 남길 수 있습니다. 둘 다 모든 작업에서 읽을 수 있고 목록 필터로 쓸 수 있습니다.

- **즐겨찾기**는 작업에 다는 북마크입니다. 상태와 관계없이 내 작업이거나, 갤러리에 있는 다른 사람의 공개된 완료 작업에 달 수 있습니다.
- **평가**는 내 작업 하나에 대한 내 판단으로, `good` 또는 `bad`입니다.

| 메서드 | 경로 | 본문 또는 쿼리 | 기능 |
| --- | --- | --- | --- |
| `PUT` | `/v1/jobs/:id/rating` | `{ "rating": "good" \| "bad" \| null }` | 내 작업 하나를 평가하거나, `null`로 평가를 지웁니다. `rating`은 필수이므로 지우기도 명시적으로 해야 합니다. `{ data: { jobId, rating } }`를 반환합니다. |
| `POST` | `/v1/gallery/favorite` | `{ "jobId": "..." }` | 작업을 즐겨찾기에 추가하거나, 이미 즐겨찾기라면 제거합니다. 전환 후의 상태인 `{ favorited }`를 반환합니다. |
| `GET` | `/v1/gallery/favorites` | 없음 | 내가 즐겨찾기한 작업 ID를 최신순으로 반환합니다: `{ data: string[] }`. |
| `GET` | `/v1/gallery` | `type`, `userId`, `favoritesOnly=true`, `limit`(1~50, 기본값 20), `cursor` | 공개 갤러리이며, 최근에 완료된 것부터 나열합니다. 내 `userId`와 내 토큰을 쓰면 `favoritesOnly=true`가 목록을 내 즐겨찾기로 좁힙니다. |

- **모든 작업에는 `favorited`와 `rating`이 담깁니다.** `GET /v1/jobs`와 `GET /v1/jobs/:id`에서입니다. 서버가 잠시 이 값을 읽을 수 없으면 작업은 그대로 응답하고 두 필드는 빠집니다. 빠진 필드는 `false`가 아니라 알 수 없는 값으로 다루세요. 알 수 없는 즐겨찾기를 전환하면 즐겨찾기가 제거될 수 있습니다.
- **평가는 작업마다 하나**이며, 작업 소유자의 평가입니다. 다른 사람의 작업은 `404 not_found`로 응답합니다. `good`, `bad`, `null` 이외의 평가는 `400 validation_error`로 응답합니다.
- **다른 사람의 비공개 작업**을 즐겨찾기하면 `404 not_found`로 응답하며, 아무것도 저장되지 않습니다. 즐겨찾기 제거는 항상 작동합니다.
- **범위:** OAuth 앱 토큰은 두 쓰기 작업에 `assets:write`가 필요하고, 내 즐겨찾기를 나열하려면 `assets:read` 또는 `jobs:read`가 필요합니다. 작업 읽기는 계속 `jobs:read`입니다.
- **갤러리를 숨기는 배포 환경**은 모든 `/v1/gallery` 라우트에서 `404`로 응답합니다.

```bash
# Rate a job good, favorite it, then list your good favorites that completed
curl -s -X PUT "https://app.nodaro.ai/v1/jobs/$JOB/rating" \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rating": "good"}'
curl -s -X POST "https://app.nodaro.ai/v1/gallery/favorite" \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"jobId\": \"$JOB\"}"
curl -s "https://app.nodaro.ai/v1/jobs?rating=good&favorited=true&status=completed" \
  -H "Authorization: Bearer $NODARO_API_KEY" | jq '.data[] | {id, favorited, rating}'
```

## 여러 작업을 한 번에 폴링
두 엔드포인트가 작업 최대 100개의 상태를 한 번의 왕복으로 반환합니다.

**GET /v1/jobs/status**

```bash
curl -s "https://app.nodaro.ai/v1/jobs/status?ids=$JOB_A,$JOB_B,$JOB_C" \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

```json
{
"jobs": [
{ "id": "0f1a9c2e-…", "status": "completed", "output_data": { "imageUrl": "https://…/a.png" }, "error_message": null, "error_hint": null, "credit_status": "committed" },
{ "id": "7d3e5f60-…", "status": "processing", "output_data": null, "error_message": null, "error_hint": null, "credit_status": "reserved" }
]
}
```

**POST /v1/jobs/batch-status**

```bash
curl -s -X POST https://app.nodaro.ai/v1/jobs/batch-status \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"jobIds\": [\"$JOB_A\", \"$JOB_B\", \"$JOB_C\"]}"
```

응답은 `{ "data": [ … ] }`입니다. 각 작업의 `id`, `status`, `output_data`, `error_message`, `error_hint`가 들어 있고, `credit_status`는 없습니다.

존재하지 않거나 다른 사람의 작업 ID는 오류 없이 제외됩니다. 응답을 내 ID 목록과 비교하세요.

## 작업 취소 또는 삭제
`POST /v1/jobs/:id/cancel`은 작업을 취소하고 작업이 예약한 크레딧을 해제합니다. 응답은 `{ "success": true, "cancelled": 1 }`입니다. `pending_review`로 보류된 작업도 취소할 수 있습니다.

`DELETE /v1/jobs/:id`는 작업과 작업이 만든 비공개 미디어를 삭제하고 `{ "success": true }`로 응답합니다. 작업 소유자만 삭제할 수 있습니다. 실행 중인 작업은 그 상태 그대로 삭제되므로, 작업을 멈춰야 한다면 먼저 취소하세요.

**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/jobs/$JOB_ID/cancel \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts
const { cancelled } = await client.jobs.cancel(jobId)
await client.jobs.delete(jobId)
```

**CLI**

```bash
nodaro jobs cancel 0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10
```

워크플로 실행 전체를 멈추려면 그 실행을 취소하세요. [실행](https://nodaro.ai/docs/developers/api/executions)을 참고하세요.

## 컴포넌트 실행
`POST /v1/component/execute`는 저장된 [컴포넌트](https://nodaro.ai/docs/nodes/automate/component)를 백그라운드에서 실행하고 `{ jobId }`와 함께 `202`로 응답합니다. 이 작업은 다른 작업처럼 폴링하세요. 서버는 컴포넌트 실행에 90분을 주고, 안에 긴 렌더링이 있으면 그 시간 예산을 더합니다. [EDL 적용](https://nodaro.ai/docs/nodes/video/apply-edl) 노드의 최종 렌더링이 그런 예입니다.

자체 시간 제한이 있는 클라이언트는 서버가 얼마나 기다릴지 물어볼 수 있습니다.

```bash
curl -s https://app.nodaro.ai/v1/component/execute/$JOB_ID/wait-limit \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

```json
{ "data": { "budgetExcessMs": 0, "waitLimitMs": 5400000, "pendingBudgetedNodes": true } }
```

- `waitLimitMs`는 이 실행에 대한 서버의 대기 시간으로, 90분에 `budgetExcessMs`를 더한 값입니다.
- `budgetExcessMs`는 실행이 긴 렌더링을 시작할 때까지 `0`입니다.
- `pendingBudgetedNodes`는 긴 렌더링이 아직 시작되지 않은 동안, 그리고 실행 자체가 아직 시작되지 않은 동안 `true`입니다. 따라서 초과분이 `0`이라고 해서 안에 긴 렌더링이 없다는 뜻은 아직 아닙니다.

두 값 모두 실행이 진행되는 동안 바뀔 수 있으므로, `waitLimitMs`에 도달하면 다시 물어보세요. 이 경로는 실행 소유자에게만 응답하며, 다른 사람과 컴포넌트 실행이 아닌 작업에는 `404`로 응답합니다. 에디터도 이 규칙을 따릅니다. 안에 긴 렌더링이 없는 실행은 30분을 기다리고, 긴 렌더링이 시작되면 `waitLimitMs`만큼, `pendingBudgetedNodes`가 `true`인 동안에는 최소 90분을 기다린 뒤 다시 물어봅니다.

## 동영상 생성 Pro 실행
[동영상 생성 Pro](https://nodaro.ai/docs/nodes/video/generate-video-pro)는 긴 동영상을 세그먼트 단위로 하나씩 만들고 세그먼트 사이마다 진행 상황을 저장하므로, 실행을 중지했다가 나중에 이어 갈 수 있습니다. Nodaro Cloud에서 실행되며, 셀프 호스팅 설치에서는 Nodaro Cloud 연결을 통해 실행됩니다.

### 실행 중지
`POST /v1/generate-video-pro/:jobId/stop`은 `processing` 상태의 실행을 안전하게 중지합니다.

- 생성 중이던 세그먼트는 버려지지만, 모델이 계속 렌더링하므로 요금은 청구됩니다.
- 남은 세그먼트는 건너뜁니다.
- 완성된 모든 세그먼트가 이어 붙여져 작업의 최종 동영상이 됩니다.
- 예약에서 사용하지 않은 부분은 환불됩니다.

응답은 `{ "jobId": "…", "stopping": true }`입니다. 작업을 계속 폴링하세요. 작업은 `output_data.pro.stopped`가 `true`로 설정되고 `stoppedAtSegment`가 포함된 채로 `completed`로 끝납니다. 아직 `pending` 상태인 작업은 대신 취소되고 전액 환불됩니다.

### 실행 이어 가기
`POST /v1/generate-video-pro/continue`는 끝난 작업에서 **새 작업**을 시작합니다. 부모 작업의 계획과 `fromSegment` 이전에 전달된 모든 세그먼트를 재사용하고, 그 지점부터 다시 생성합니다.

```json
{ "fromJobId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "fromSegment": 4 }
```

- `fromSegment`는 1부터 셉니다. 기본값은 전달되지 않은 첫 번째 세그먼트입니다.
- 부모 작업은 끝난 상태여야 합니다. 중지되었거나, 전달된 세그먼트가 하나 이상 있는 상태로 실패했거나, 완료된 작업이어야 합니다. 완료된 실행에 `fromSegment`를 명시하면 결말을 다시 생성합니다.
- 다시 생성한 세그먼트와 동영상 생성 Pro의 고정 요금만 지불합니다.
- 이 경로는 `Idempotency-Key` 헤더를 지원하므로, 재시도한 요청이 두 번째 작업을 시작하지 않습니다.

응답은 `{ jobId, continuedFromJobId, fromSegment, segmentCount }`입니다. 새 `jobId`를 폴링하세요.

**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/generate-video-pro/$JOB_ID/stop \
  -H "Authorization: Bearer $NODARO_API_KEY"

curl -s -X POST https://app.nodaro.ai/v1/generate-video-pro/continue \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4c1f0a7e-2b3d-4e5f-8a9b-0c1d2e3f4a5b" \
  -d "{\"fromJobId\": \"$JOB_ID\", \"fromSegment\": 4}"
```

**TypeScript SDK**

```ts
await client.videoPro.stop(jobId)
const { data } = await client.jobs.getStatus(jobId) // completes with a partial video

const { jobId: childId } = await client.videoPro.continueRun(jobId, { fromSegment: 4 })
```

**CLI**

```bash
nodaro video-pro stop 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
nodaro video-pro continue 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d --from-segment 4 --watch
```

두 경로 모두 내 작업이 아니면 `404`로, 동영상 생성 Pro 실행이 아닌 작업이면 `400`으로 응답합니다.

### 실행 견적
`POST /v1/credits/video-pro-estimate`는 작업을 만들거나 크레딧을 예약하지 않고 동영상 생성 Pro 실행의 가격을 계산합니다.

```json
{ "provider": "gemini-omni-flash", "resolution": "720p", "duration": 12, "renderMethod": "keyframes", "segmentMode": "short" }
```

한 예시 구성에서 응답은 `{ "data": { "credits": 760, "upperBound": true } }`입니다. 현재 가격은 실제 응답에서 확인하세요.

<TypeTable
type={{
provider: { type: 'string', description: '동영상 모델입니다.', required: true },
resolution: { type: 'string', description: '해상도입니다.', default: '720p' },
duration: { type: 'integer', description: '전체 길이(초)이며, 1~3600입니다.', default: '8' },
aspectRatio: { type: 'string', description: '프레임의 형태입니다.' },
renderMethod: { type: 'string', description: 'extend 또는 keyframes입니다.' },
anchorMode: { type: 'string', description: 'upfront, progressive, none 중 하나입니다.' },
contextTailSec: { type: 'number', description: '2~15초입니다.' },
segmentMode: { type: 'string', description: 'short, long, max 중 하나입니다. preferredSegmentSec 또는 segmentDurations와 함께 쓸 수 없습니다.' },
preferredSegmentSec: { type: 'integer', description: '4~15초입니다.' },
segmentDurations: { type: 'integer[]', description: '세그먼트 길이를 1~24개 명시합니다. 각 길이는 1~30초입니다.' },
planOnly: { type: 'boolean', description: '계획 단계의 가격만 계산합니다.' },
}}
/>

- `segmentMode`가 `short` 또는 `long`이면, 실행은 먼저 원본의 구간에 동작을 통째로 배정합니다. `upperBound: true`는 이 수치가 계획 전의 예약 한도라는 뜻입니다. 최종 청구액은 실제 계획을 따릅니다.
- `planOnly` 견적은 계획 수수료만 포함하며 `upperBound: false`를 반환합니다. 응답의 `sourceSegmentDurations`와 `planCheckpoint`는 같은 모드와 설정으로 각각 `sourceSegmentDurations`와 `seedPlan`으로 다시 보낼 수 있습니다.

세그먼트 분할 방식과 각 설정의 역할은 [동영상 생성 Pro](https://nodaro.ai/docs/nodes/video/generate-video-pro)를 읽어 보세요.

## 자주 묻는 질문

### Nodaro API에서 작업이란 무엇인가요?

작업은 이미지 1장, 동영상 렌더링 1회, 음성 클립 1개처럼 생성의 한 단위입니다. POST /v1/ 뒤에 노드 타입을 붙여 보내면 작업 ID가 반환되고, 워크플로를 실행하면 실행하는 AI 노드마다 작업이 하나씩 만들어집니다.

### Nodaro 작업은 얼마나 자주 폴링해야 하나요?

2~5초마다 폴링하세요. 작업 상태는 밀리초가 아니라 초 단위로 바뀝니다. 여러 작업을 추적하려면 한 번의 호출로 작업을 최대 100개까지 반환하는 GET /v1/jobs/status 또는 POST /v1/jobs/batch-status를 사용하세요.

### pending_review 상태는 무슨 뜻인가요?

배포 환경이 결과를 공개하기 전에 사람의 검토를 위해 보류하고 있다는 뜻입니다. 작업은 아직 진행 중이며 크레딧은 예약된 상태로 유지됩니다. 계속 폴링하세요. 작업은 completed, failed, cancelled 중 하나로 끝납니다.

### 실패한 작업이 환불되었는지 어떻게 알 수 있나요?

GET /v1/jobs/:id 또는 GET /v1/jobs/:id/status에서 credit_status를 확인하세요. 값은 reserved, committed, refunded 중 하나입니다. 안전 필터나 배포 환경의 정책으로 차단된 생성은 항상 환불됩니다.

### 작업에 실제로 든 비용은 어떻게 알 수 있나요?

GET /v1/jobs 또는 GET /v1/jobs/:id에서 credits_charged를 확인하세요. 예약이 정산된 뒤 작업에 든 비용이며, 예약된 금액인 credits보다 적을 수 있습니다. 환불 후에는 0이고, 작업이 실행 중일 때는 null입니다.

### API로 작업을 평가하거나 즐겨찾기에 추가할 수 있나요?

네. PUT /v1/jobs/:id/rating은 내 작업 하나에 good, bad 또는 null을 설정하고, POST /v1/gallery/favorite는 즐겨찾기를 켜고 끕니다. GET /v1/jobs는 rating=과 favorited=로 둘 다 필터링합니다.
