# 트렌드

> Nodaro 트렌드 카탈로그를 둘러보고, 트렌드의 슬롯을 내 사진으로 채우고, 무료로 견적을 받은 뒤 REST, SDK, CLI, MCP로 실행하세요.

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

**트렌드 API**는 미리 만들어진 [Cast Swap](https://nodaro.ai/docs/developers/api/cast-swap) 레시피를 실행합니다. 트렌드는 슬롯이 있는 짧은 클립입니다. 슬롯은 교체할 수 있는 인물, 반려동물, 제품, 장소입니다. 슬롯을 내 사진으로 채우고 트렌드를 실행하면 그 사진이 들어간 클립을 받습니다.

트렌드는 Nodaro Cloud의 기능입니다. 셀프 호스팅 설치에서는 모든 라우트가 `404`로 응답합니다. 라우트는 Bearer 토큰을 받습니다. [인증](https://nodaro.ai/docs/developers/api/authentication)을 참조하세요.

여기에는 **게시된** 트렌드만 있습니다. 초안, 게시되지 않은 슬러그, 알 수 없는 슬러그는 모든 라우트에서 `404`입니다.

## 엔드포인트
| 메서드 | 경로 | 하는 일 |
| --- | --- | --- |
| `GET` | `/v1/trends` | 카탈로그 자체의 순서로 된 카탈로그입니다. `{ data: TrendCard[], next }`. 쿼리: `q`(검색), `tag`, `favorites=1`(내 즐겨찾기만), `locale`, `cursor`, `limit`(1~60, 기본값 30). |
| `GET` | `/v1/trends/:slug` | 트렌드 하나입니다. `{ data: TrendDetail }`이며, 슬롯, 예시, 미리보기 길이, 해상도가 들어 있습니다. 쿼리: `locale`. |
| `POST` | `/v1/trends/:slug/estimate` | 실행 견적을 받습니다. 무료입니다. `{ data: { expectedCredits, holdCredits, breakdown } }`로 응답합니다. |
| `POST` | `/v1/trends/:slug/runs` | 실행을 시작합니다. `202 { data: { jobId } }`로 응답합니다. `GET /v1/jobs/:id`로 작업을 추적하세요. |
| `GET` | `/v1/trends/runs` | 내 트렌드 실행을 최신순으로 반환합니다. `{ data: TrendRun[], next }`. 쿼리: `trend`(슬러그), `cursor`, `limit`(1~60, 기본값 30). |
| `PUT` | `/v1/trends/:slug/favorite` | 트렌드를 즐겨찾기에 추가합니다. `204`로 응답합니다. |
| `DELETE` | `/v1/trends/:slug/favorite` | 트렌드를 즐겨찾기에서 제거합니다. `204`로 응답합니다. |

이름, 설명, 슬롯 레이블, 힌트는 `?locale=`이 플랫폼 언어이면 그 언어로 옵니다. 그렇지 않으면 내 프로필의 언어로, 그다음에는 영어로 옵니다.

트렌드가 새 버전으로 바뀌거나 게시가 취소된 뒤에도 실행은 `GET /v1/trends/runs`에서 계속 읽을 수 있습니다.

## 트렌드에 담긴 것
`GET /v1/trends/:slug`는 다음을 반환합니다.

- `slots`: 각 슬롯의 `key`, `label`, `kind`(`person`, `pet`, `product`, `location` 중 하나), `required`, 선택 사항인 `hint`, 기본 칩.
- `examples`: 각 예시의 결과 동영상과 각 슬롯이 쓴 사진.
- `previewSec`: 트렌드가 4초 미리보기 실행을 제공하면 `4`, 아니면 `null`.
- `resolutions`: 트렌드를 실행할 수 있는 해상도.

`GET /v1/trends`의 카탈로그 카드에는 슬러그, 이름, 태그, `needs`(종류별 슬롯 수), 길이(초), `isFavorite`가 담깁니다.

## 실행 본문
`POST /v1/trends/:slug/estimate`와 `POST /v1/trends/:slug/runs`는 같은 본문 `{ slots, example?, details?, preview?, resolution?, draft?, sessionId?, projectId? }`를 받습니다.

| 필드 | 하는 일 |
| --- | --- |
| `slots` | 필수입니다. 슬롯 키 → `{ images?, …chips }`이며, 슬롯마다 한 대상의 이미지 1~3장을 받습니다. 필수 슬롯에는 모두 이미지가 있어야 합니다. |
| `example` | 선택 사항입니다. 1부터 시작하는 예시 번호입니다. 비워 둔 선택 슬롯을 이 예시의 사진이 채웁니다. 기본값은 1입니다. |
| `details` | 선택 사항입니다. 내 문구로, 최대 1,000자입니다. 슬롯은 `{slot:<key>}`로 언급하세요. |
| `preview` | 선택 사항입니다. 트렌드의 `previewSec`가 4이면 `true`로 4초를 실행합니다. |
| `resolution` | 선택 사항입니다. 기본값인 `"480p"`, `"720p"`, `"1080p"` 중 하나입니다. |
| `draft` | 선택 사항입니다. 기본값은 `true`입니다. 초안은 480p에서만 쓸 수 있습니다. |
| `sessionId` | 선택 사항입니다. 받아서 확인하지만 아직 적용되지 않습니다. 트렌드 실행은 아직 세션에 보관되지 않습니다. |
| `projectId` | 선택 사항입니다. 받지만 현재 실행에는 쓰이지 않습니다. |

칩은 슬롯 종류의 고유 특성입니다. 각 칩은 내 사진으로 만드는 `"new"`, 또는 클립 그대로 유지하는 `"original"`입니다.

| 슬롯 종류 | 칩 |
| --- | --- |
| `person` | `body`, `outfit`, `hair` |
| `pet` | `body`, `coat` |
| `product` | `shape`, `finish` |
| `location` | 없음 |

```json
{
"slots": {
"dancer": { "images": ["https://example.com/me.jpg"], "outfit": "original" }
},
"preview": true,
"resolution": "480p"
}
```

## 견적
견적은 실행에 청구될 것으로 예상되는 `expectedCredits`와, 실행이 시작될 때 예약되는 `holdCredits`로 응답합니다. `breakdown`은 견적의 구성 항목을 나열합니다.

## 반복 요청
`POST /v1/trends/:slug/runs`는 8~200자의 `Idempotency-Key` 헤더를 받습니다. 같은 키로 반복된 요청은 첫 실행의 작업으로 응답합니다. 새로 시작되거나 청구되는 것은 없습니다.

## 권한
OAuth 앱에는 다음이 필요합니다.

- 카탈로그, 트렌드, 내 실행을 읽으려면 `jobs:read`.
- 실행 견적을 받거나 시작하려면 `workflows:execute`, `sessionId`를 보내려면 `assets:write`도.
- 즐겨찾기에는 `assets:write`.

## 오류
거부의 본문은 `{ error: { code, message } }`입니다. 슬롯 오류에는 슬롯의 키도 `slot`으로 담깁니다. 견적 라우트와 실행 라우트의 `400` 거부는 아무것도 청구되기 전에 일어납니다.

| 상태 | `code` | 발생 시점 |
| --- | --- | --- |
| `400` | `validation_error` | 본문이나 쿼리가 유효하지 않거나, `cursor`가 이 목록이 준 값이 아니거나, `Idempotency-Key`가 8~200자가 아닙니다. |
| `400` | `missing_slot` | 필수 슬롯에 이미지가 없거나, 선택 슬롯을 비워 두었는데 선택한 예시에 그 슬롯의 사진이 없습니다. |
| `400` | `unknown_slot` | `slots`가 트렌드에 없는 키를 지정합니다. |
| `400` | `invalid_choice` | 슬롯의 종류가 받지 않는 칩입니다. 예를 들어 인물의 `coat`나, 장소에 붙은 칩입니다. |
| `400` | `example_out_of_range` | `example`이 트렌드에 없는 예시를 지정하거나, 선택 슬롯을 비워 두었는데 트렌드에 그 슬롯을 채울 예시가 없습니다. |
| `400` | `preview_unavailable` | 미리보기를 제공하지 않는 트렌드에 `preview: true`를 보냈습니다. |
| `400` | `invalid_slot_mention` | `details`가 트렌드에 없는 키로 `{slot:<key>}`를 언급합니다. |
| `400` | `prompt_too_long` | 이 트렌드에는 `details`가 너무 깁니다. 줄이세요. |
| `401` | `unauthorized` | 인증된 사용자가 없습니다. |
| `404` | `not_found` | 트렌드가 게시되지 않았거나, `sessionId`가 내가 쓸 수 없는 세션을 가리킵니다. |
| `502` | `engine_error` | 동영상 엔진이 예상하지 못한 방식으로 응답했습니다. |
| `503` | `engine_unsupported` | 이 플랫폼 버전에서는 아직 트렌드를 실행할 수 없습니다. |
| `503` | `trend_unavailable` | 지금은 트렌드를 실행할 수 없습니다. |
| `503` | `not_available` | 이 서버에는 아직 트렌드 카탈로그가 없습니다. `GET /v1/trends/runs`를 제외한 모든 라우트에 해당합니다. |

동영상 엔진이 견적이나 실행을 거부하면, 트렌드 라우트는 자체 코드와 고정 메시지로 응답합니다.

| 상태 | `code` | 발생 시점 |
| --- | --- | --- |
| `400` | `trend_run_rejected` | 엔진이 아래에 없는 이유로 이 입력을 거부했습니다. |
| `400` | `clip_out_of_range`, `preview_out_of_range`, `video_unreadable` | 트렌드 자체의 클립을 있는 그대로 실행할 수 없습니다. |
| `400` | `unsafe_url` | 사진이 공개 http 또는 https 주소가 아닙니다. |
| `400` | `raw_reference_token` | `details`에 `{image:`, `{video:`, `{audio:`가 들어 있습니다. |
| `402` | `insufficient_credits` | 내 크레딧으로 실행 비용을 감당할 수 없습니다. 워크스페이스에서는 거부한 한도에 따라 `budget_exceeded`, `member_cap_exceeded`, `user_allowance_exceeded`로 응답할 수도 있습니다. |
| `413` | `storage_limit_exceeded` | 내 저장 공간이 가득 찼습니다. |
| `422` | `job_blocked` | 배포 환경의 작업 정책이 실행을 막았습니다. |
| `429` | `rate_limited` | 요청이 너무 많습니다. 잠시 후 다시 시도하세요. |
| `503` | `engine_unsupported`, `estimate_unavailable` | 지금은 실행하거나 견적을 낼 수 없습니다. 잠시 후 다시 시도하세요. |
| `502` | `engine_error` | 그 밖의 모든 경우입니다. |

## 끝나지 않은 실행
`GET /v1/trends/runs`에서 끝나지 않은 실행에는 `error: { code, message }`가 있습니다. 메시지는 항상 같은 고정 문장이므로 코드로 분기하세요.

| `code` | 의미 |
| --- | --- |
| `safety-block`, `policy-block` | 작업이 차단되었습니다. |
| `credits` | 실행 도중 크레딧이 부족해졌습니다. 환불되었습니다. 크레딧을 충전한 뒤 다시 실행하세요. |
| `cancelled` | 실행이 취소되었습니다. |
| `failed` | 그 밖의 모든 실패입니다. |

실패하지도 취소되지도 않은 실행은 `error: null`입니다. 완료된 실행에는 `videoUrl`, `posterUrl`, `creditsCharged`가 담깁니다. 실행의 각 슬롯은 그 사진이 예시에서 왔는지(`fromExample`) 알려 줍니다.

## SDK에서 사용
`client.trends`는 이 라우트들을 감쌉니다. `@nodaro/sdk/trends`로도 가져올 수 있습니다.

```ts
list(params?: ListTrendsParams): Promise<TrendPage<TrendCard>>
get(slug: string, params?: { locale?: string }): Promise<TrendDetail>
estimate(slug: string, request: TrendRunRequest): Promise<TrendEstimate>
run(slug: string, request: TrendRunRequest, options?: { idempotencyKey?: string }): Promise<{ jobId: string }>
runs(params?: ListTrendRunsParams): Promise<TrendPage<TrendRun>>
favorite(slug: string): Promise<void>
unfavorite(slug: string): Promise<void>
```

`list()`와 `runs()`는 도착한 그대로의 페이지 `{ data, next }`를 반환합니다. `next`를 `cursor`로 다시 넘기세요. 마지막 페이지에서는 `null`입니다. `get()`, `estimate()`, `run()`은 `{ data }` 봉투 없이 페이로드를 반환합니다. `"runs"`는 슬러그가 될 수 없습니다. 슬러그를 받는 메서드는 요청을 보내기 전에 이 값에 대해 `TypeError`를 발생시킵니다.

```ts

const { data: cards } = await client.trends.list({ q: "dance" })
const trend = await client.trends.get(cards[0].slug)

const request: TrendRunRequest = {
// Every required slot needs 1–3 pictures; optional slots left out take the example's.
slots: Object.fromEntries(
trend.slots.filter((s) => s.required).map((s) => [s.key, { images: [myPhotoUrl] }]),
),
}
const quote = await client.trends.estimate(trend.slug, request)
console.log(`about ${quote.expectedCredits} credits, ${quote.holdCredits} held while it runs`)

const { jobId } = await client.trends.run(trend.slug, request, { idempotencyKey: crypto.randomUUID() })
const job = await client.jobs.get(jobId)
```

## CLI에서 사용
```bash
nodaro trends list --search dance
nodaro trends get <slug>
nodaro trends estimate <slug> --slot dancer=https://example.com/me.jpg --choice dancer.outfit=original
nodaro trends run <slug> --slot dancer=https://example.com/me.jpg --idempotency-key <key>
nodaro trends runs --trend <slug>
nodaro trends favorite <slug>
```

`--slot`은 반복할 수 있으며, 슬롯마다 사진 1~3장을 받습니다. `trends run`은 크레딧을 쓰므로 먼저 `trends estimate`를 실행하세요. [CLI 명령](https://nodaro.ai/docs/developers/cli/commands)을 참조하세요.

## MCP에서 사용
네 MCP 도구가 트렌드를 다룹니다. `list_trends`, `get_trend`, `estimate_trend`, `run_trend`입니다. [MCP › 동영상](https://nodaro.ai/docs/mcp/tools/video#trends)을 참조하세요. `confirm` 없이 호출한 `run_trend`는 견적을 반환하고 아무것도 청구하지 않습니다. 사용자가 가격을 수락한 뒤 `confirm: true`로 호출하면 실행을 시작하고 작업 ID를 반환합니다.

## 자주 묻는 질문

### Nodaro에서 트렌드란 무엇인가요?

트렌드는 미리 만들어진 Cast Swap 레시피입니다. 교체할 수 있는 인물, 반려동물, 제품, 장소를 위한 슬롯이 있는 짧은 클립입니다. 슬롯을 내 사진으로 채우면 그 사진이 들어간 클립을 받습니다.

### 트렌드 실행 비용은 어떻게 알 수 있나요?

실행할 때와 같은 본문으로 POST /v1/trends/:slug/estimate를 호출하세요. 무료입니다. 실행에 청구될 것으로 예상되는 expectedCredits와, 실행 중에 예약되는 holdCredits를 반환합니다.

### 모든 슬롯을 채워야 하나요?

아니요. 필수 슬롯에는 모두 사진 1~3장이 필요합니다. 비워 둔 선택 슬롯은 트렌드 예시의 사진을 쓰며, 기본값은 첫 번째 예시입니다.

### 트렌드 실행을 안전하게 재시도할 수 있나요?

네. 8~200자의 같은 Idempotency-Key 헤더를 보내세요. 같은 키로 반복된 요청은 첫 실행의 작업으로 응답하며, 새로 시작되거나 청구되는 것은 없습니다.

### 트렌드는 셀프 호스팅 설치에서도 작동하나요?

아니요. 트렌드는 Nodaro Cloud의 기능입니다. 셀프 호스팅 설치에서는 모든 트렌드 라우트가 404로 응답합니다.
