# 업로드

> POST /v1/upload로 이미지, 동영상, 오디오를 업로드합니다. URL에서 파일 복사, SNS 동영상 가져오기, 저장된 미디어 자르기, 라이브러리 검색, 파일 링크도 다룹니다.

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

**업로드**는 내 이미지, 동영상, 오디오를 Nodaro의 저장 공간에 넣어 어떤 노드든 URL로 사용할 수 있게 합니다. `POST /v1/upload`는 파일 하나를 저장하고 그 URL을 반환합니다. 다른 경로는 URL에서 파일을 복사하거나, SNS에서 동영상을 가져오거나, 저장된 파일을 무료로 자르거나, 저장한 미디어 목록을 보여 줍니다.

## 엔드포인트
| 메서드 | 경로 | 기능 |
| --- | --- | --- |
| `POST` | `/v1/upload` | 파일 하나를 multipart 폼 데이터로 업로드합니다. |
| `POST` | `/v1/save-to-storage` | URL의 파일을 작업으로 내 저장 공간에 복사합니다. |
| `POST` | `/v1/download-video` | SNS나 동영상 직접 링크에서 동영상을 가져옵니다. |
| `GET` | `/v1/download-video/progress/:downloadId` | 가져오기 진행률을 서버 전송 이벤트로 보냅니다. |
| `POST` | `/v1/video-metadata` | 동영상을 다운로드하지 않고 길이, 크기, 제목을 읽습니다. |
| `POST` | `/v1/media/process` | 저장된 동영상이나 오디오 파일을 자르거나 크롭합니다. 무료이며 동기식입니다. |
| `GET` | `/v1/library` | 내 미디어 라이브러리를 최신순으로, 검색과 필터와 함께 반환합니다. |
| `POST` | `/v1/library/save-generated` | 내 파일 하나를 라이브러리에 저장합니다. |

## 파일 업로드
파일을 `file` 필드에 담아 multipart 폼 데이터로 보내세요. `type`을 `image`, `video`, `audio` 중 하나로 함께 보낼 수도 있습니다.

**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/upload \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -F "file=@portrait.png;type=image/png" \
  -F "type=image"
```

```json
{
"data": {
"url": "https://…/portrait.png",
"assetId": "5d6e7f80-91a2-4b3c-8d4e-5f60718293a4",
"thumbnailUrl": "https://…/portrait-thumb.webp",
"category": "image",
"filename": "portrait.png",
"mimeType": "image/png",
"sizeBytes": 1482233,
"r2Key": "…/portrait.png"
}
}
```

**TypeScript SDK**

```ts
// In a browser, file comes from an <input type="file">.
// In Node 20 or newer, build it from bytes:

const file = new File([await readFile('portrait.png')], 'portrait.png', { type: 'image/png' })
const upload = await client.uploads.upload(file)
console.log(upload.url) // pass it as imageUrl, videoUrl or audioUrl
```

| 필드 | 의미 |
| --- | --- |
| `url` | 저장된 파일의 공개 URL입니다. 어떤 노드에서든 사용할 수 있습니다. |
| `assetId` | 내 저장 공간에 있는 파일의 ID입니다. |
| `thumbnailUrl` | 이미지와 동영상의 썸네일입니다. 오디오는 `null`입니다. |
| `category` | 서버가 분류한 파일 종류로, `image`, `video`, `audio` 중 하나입니다. |
| `filename` | 파일의 표시 이름입니다. |
| `mimeType` | 서버가 최종적으로 정한 파일 형식입니다. 아래를 참고하세요. |
| `sizeBytes` | 저장된 크기(바이트)입니다. |
| `r2Key` | 저장 공간에서 파일의 키입니다. |

CLI에는 업로드 명령이 없습니다. 이미 온라인에 있는 파일을 터미널에서 내 저장 공간으로 복사하려면 `nodaro media save <url>`을 사용하세요.

### 지원 형식과 파일 유형
| 미디어 | 형식 |
| --- | --- |
| 이미지 | PNG, JPEG, WebP |
| 동영상 | MP4, MOV, WebM |
| 오디오 | MP3, WAV, M4A 및 AAC, OGG, WebM, FLAC. 최대 50MB. |

미디어 종류마다 크기 한도가 따로 있습니다. 클라이언트가 선언하는 형식이 표준 형식일 필요는 없습니다. 서버가 파일을 확인하기 전에 실제 형식을 알아내기 때문입니다.

- **파라미터가 제거됩니다.** `audio/webm;codecs=opus`는 `audio/webm`이 됩니다.
- **흔히 쓰는 다른 표기는 표준 형식으로 바뀝니다.** Windows가 일반 `.aac` 파일에 쓰는 `audio/vnd.dlna.adts`는 `audio/aac`가 되고, `image/jpg`는 `image/jpeg`, `video/mov`는 `video/quicktime`이 됩니다.
- **쓸모없는 형식은 파일 이름으로 판단합니다.** `application/octet-stream`이거나 형식이 아예 없으면 확장자가 결정합니다. `clip.mp4`는 `video/mp4`로 저장됩니다.

그래도 허용된 형식과 맞지 않으면 허용 형식 목록과 함께 `400 validation_error`로 응답합니다. 서버가 최종적으로 정한 형식은 `mimeType`으로 반환되며, 파일도 이 형식으로 제공됩니다.

**MP4와 MOV 동영상은 색인을 앞에 두고 저장됩니다.** OBS나 휴대폰으로 녹화한 파일은 이 색인을 파일 끝에 두는 경우가 많아서, 큰 파일의 첫 탐색이 느립니다. 모든 동영상 업로드 경로는 이런 파일을 저장하기 전에 한 번, 스트림을 새 컨테이너로 복사해 다시 씁니다. 다시 인코딩하지 않으므로 화면과 소리는 똑같습니다. 이미 색인이 앞에 있는 파일이나 다시 쓴 결과를 검증할 수 없는 파일은 보낸 그대로 저장됩니다. 다시 쓴 파일은 새로 감싸지므로, 저장된 크기가 보낸 바이트 수와 조금 다를 수 있습니다. `sizeBytes`는 항상 저장된 파일의 길이이므로, 로컬 파일 크기나 체크섬과 비교하지 마세요.

### 업로드가 거부되는 경우
| 상태 | 코드 | 의미 |
| --- | --- | --- |
| 400 | `validation_error` | 파일이 허용된 형식이 아닙니다. |
| 413 | | 저장 공간이 가득 찼습니다. SDK는 `limitBytes`에 한도를 담은 `StorageExceededError`를 발생시킵니다. |
| 422 | `upload_blocked` | 배포 환경의 업로드 정책이 파일을 저장하기 전에 거부했습니다. `message`를 그대로 사용자에게 보여 주세요. 이런 정책이 있는 배포 환경에서만 발생합니다. |

## 대신 URL 사용
대부분의 경로는 `imageUrl`, `videoUrl`, `audioUrl`, `referenceImageUrls`처럼 미디어를 URL로 받으므로, 이미 온라인에 있는 파일은 업로드할 필요가 없습니다. URL은 공개 `http` 또는 `https` 주소여야 합니다. 사설 네트워크 주소를 가리키거나 다른 스킴을 쓰는 URL은 거부됩니다.

만료되는 서명 링크처럼 URL이 임시일 때나 파일을 계속 쓸 수 있게 두고 싶을 때는 파일을 Nodaro의 저장 공간에 복사하세요.

**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/save-to-storage \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mediaUrl": "https://example.com/clip.mp4", "filename": "clip.mp4", "mediaType": "video"}'
# {"jobId":"…"}  poll the job; its output carries the stored file
```

**TypeScript SDK**

```ts
const { jobId } = await client.media.saveToStorage({
mediaUrl: 'https://example.com/clip.mp4',
filename: 'clip.mp4',
mediaType: 'video',
})
```

**CLI**

```bash
nodaro media save https://example.com/clip.mp4 --filename clip.mp4 --type video --watch
```

워크플로에서는 업로드한 파일의 URL을 실행 입력으로 [이미지 업로드](https://nodaro.ai/docs/nodes/image/upload-image), [동영상 업로드](https://nodaro.ai/docs/nodes/video/upload-video), [오디오 업로드](https://nodaro.ai/docs/nodes/audio/upload-audio) 노드에 전달하세요. 입력 노드의 미디어 `url` 필드는 실행에서 언제든 덮어쓸 수 있습니다. [새 입력 값으로 워크플로 실행](https://nodaro.ai/docs/developers/api/workflows#run-a-workflow-with-new-input-values)을 참고하세요.

## SNS에서 동영상 가져오기
`POST /v1/download-video`는 YouTube, TikTok, Instagram, X, Facebook의 동영상이나 동영상 파일 직접 링크를 내 저장 공간으로 가져옵니다. 응답은 작업 ID가 아니라 `downloadId`입니다.

```json
{ "url": "https://youtu.be/XXXX", "maxHeight": 720, "sectionStartSec": 30, "sectionEndSec": 90 }
```

- `maxHeight`는 해상도를 제한합니다. 생략하면 가능한 최고 해상도로 가져옵니다. `sectionStartSec`과 `sectionEndSec`은 동영상의 해당 부분만 가져오며, 컷이 키프레임에 맞춰지기 때문에 앞뒤로 약 3초씩 더 포함됩니다. 둘 다 보내거나 둘 다 생략하세요. 정확하게 잘라야 하면 결과를 자르세요.
- 사운드 트랙이 없는 동영상은 실패합니다. `requireAudio: false`를 보내면 예외입니다.
- 계정마다 가져오기를 동시에 4개까지 실행할 수 있습니다. 다섯 번째는 `429 too_many_downloads`로 응답합니다.
- `GET /v1/download-video/progress/:downloadId`는 진행률을 서버 전송 이벤트로 1초에 약 두 번 스트리밍합니다: `{ phase, percent, videoUrl?, thumbnailUrl?, error? }`. 가져오기가 `completed`가 되면 저장된 `videoUrl`과 함께, `failed`가 되면 `error`와 함께 스트림이 끝납니다. 진행률은 가져오기가 끝난 뒤 잠시만 보관되므로 바로 읽기 시작하세요.
- 완성된 동영상은 내 라이브러리에 저장됩니다.

가져오기 전에 동영상을 확인하려면 `{ url }`로 `POST /v1/video-metadata`를 호출하세요. 다운로드하지 않고 길이, 크기, 제목을 읽습니다. 터미널에서는 `nodaro media download <url> --max-height 720 --watch`와 `nodaro media metadata <url>`을 사용하세요. 다른 미디어 도구는 [보이스 및 미디어](https://nodaro.ai/docs/developers/api/voice-and-media)에 있습니다.

## 저장된 파일 자르기 또는 크롭
`POST /v1/media/process`는 저장된 동영상이나 오디오 파일을 자르거나 크롭합니다. 무료이며 작업 없이 바로 응답합니다.

```json
{
"sourceUrl": "https://…/interview.mp4",
"type": "video",
"trim": { "startTime": 12.5, "endTime": 48 },
"crop": { "x": 0, "y": 0, "width": 1080, "height": 1080 }
}
```

응답은 `{ "data": { url, thumbnailUrl, assetId, sizeBytes, mimeType, metadata } }`입니다. `format`은 출력 형식을 정합니다. 동영상은 `mp4` 또는 `webm`, 오디오는 `mp3`, `wav`, `m4a`, `aac`입니다. `deleteSource: true`를 보내면 원본 파일이 내 것이고 다른 곳에서 쓰지 않을 때 작업 후 원본을 삭제하므로, 잘라 낸 파일이 원본을 대신합니다. SDK에서는 `client.media.process(input)`을 호출하세요. 워크플로 안에서 자르려면 크레딧이 드는 [동영상 자르기](https://nodaro.ai/docs/nodes/video/trim-video) 노드를 사용하세요.

## 내 미디어 라이브러리
`GET /v1/library`는 내 미디어를 최신순으로 나열합니다. 기본적으로 내가 라이브러리에 저장한 파일과 공유 라이브러리를 나열합니다. `owned=true`를 주면 대신 내가 소유한 모든 파일을 나열합니다. 저장 여부와 관계없이 업로드와 생성 결과가 모두 포함됩니다.

| 쿼리 | 의미 |
| --- | --- |
| `type` | `all`(기본값), `image`, `video`, `audio`. |
| `q` | 검색: 파일 이름에 `q`의 단어가 모두 들어 있는 파일만 가져오며, 최대 5단어까지이고 대소문자는 구분하지 않습니다. 모든 문자는 그 문자 그대로 비교하므로 `%`, `_`, `*`는 와일드카드가 아닙니다. 빈 `q`는 검색하지 않으며, `q`는 최대 200자까지 받습니다. 이전 이름인 `search`도 계속 작동합니다. |
| `owned` | `true`이면 라이브러리 보기 대신 내가 소유한 모든 파일을 나열합니다. |
| `source` | 한 곳에서 온 미디어만: `internal`, `mcp`, `app`, `cli`, `sdk`, `extension`, `web`, `api`. 이 값이 기록되기 전의 파일에는 출처가 없으므로, `source`를 생략했을 때만 나타납니다. |
| `limit` | 페이지 크기, 1~100. 기본값은 40입니다. |
| `cursor` | 이전 페이지의 `nextCursor`. |

응답은 `{ data, nextCursor, totalCount }`입니다. 각 항목에는 `id`, `type`, `filename`, `mimeType`, `sizeBytes`, `url`, `thumbnailUrl`, `metadata`, `isLibraryItem`, `uploadSource`, `source`, `sourceDetail`, `jobId`, `createdAt`이 담깁니다. `jobId`는 파일을 만든 작업이며, 업로드라면 `null`입니다. `nextCursor`는 마지막 페이지에서 `null`이고, 일치하는 파일 수인 `totalCount`는 첫 페이지에만 담깁니다.

```bash
curl -s "https://app.nodaro.ai/v1/library?owned=true&type=image&q=poster" \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

SDK에서는 `client.library.list({ type: 'image', q: 'poster' })`를 호출하세요. MCP 도구 `browse_uploads`는 `owned=true` 보기를 읽으며, 같은 파일 이름 검색을 `search` 인수로 받습니다.

`{ url, type }`과 함께 `POST /v1/library/save-generated`를 보내면 파일을 내 라이브러리에 저장합니다. Nodaro 저장소에 있는 파일은 내 파일일 때만 받습니다. 내 생성 결과, 내가 업로드한 파일, 내 업로드 폴더의 파일입니다. Nodaro 저장소의 다른 파일은 존재 여부와 관계없이 `403 forbidden`으로 응답합니다. 다른 사이트의 링크는 받은 그대로 저장됩니다.

### 사용자를 파일 하나로 연결
로그인한 사용자를 앱의 파일 라이브러리인 **내 파일**의 파일 하나로 보내려면, 두 쿼리 매개변수 중 하나를 붙여 `/library`로 링크하세요.

| 링크 | 여는 곳 |
| --- | --- |
| `https://app.nodaro.ai/library?job=<jobId>` | 그 작업이 만든 파일: 생성 호출이 반환한 `jobId`입니다. |
| `https://app.nodaro.ai/library?asset=<assetId>` | 그 파일: `GET /v1/library?owned=true`의 `id`입니다. `owned=true`가 없으면 목록에 내 파일에는 절대 표시되지 않는 공유 라이브러리 항목도 들어 있습니다. |

- **페이지는 먼저 찾은 다음 파일을 보여 줍니다.** “파일을 찾는 중…”을 표시한 뒤, 파일 위치로 스크롤하고 표시한 다음 미리 보기를 엽니다.
- **사용자의 최신 파일 약 1,000개 안에서 찾습니다.** 더 오래된 파일의 링크는 그 파일 없이 내 파일을 엽니다.
- **파일을 보여 줄 수 없으면 짧은 메시지가 이유를 알려 주고**, 페이지는 평소처럼 열립니다. id를 알 수 없거나 사용자의 것이 아닐 때, 작업이 아직 실행 중일 때, 작업이 실패했거나 파일을 만들지 않았을 때입니다. 찾기 자체가 실패하면 메시지가 링크를 다시 시도해 달라고 안내합니다.
- **매개변수는 읽은 뒤 주소에서 제거되므로**, 새로 고침이나 **뒤로**를 눌러도 파일이 다시 열리지 않습니다.
- **로그인하지 않은 사용자는** 먼저 로그인한 다음 파일로 이동합니다. 셀프 호스팅 설치는 자체 주소에서 같은 경로에 응답합니다.

## AI 어시스턴트에서 업로드
[MCP 서버](https://nodaro.ai/docs/mcp/tools)는 업로드 방법 세 가지를 제공하며, 각각 이미지, 오디오, 동영상용이 있습니다. 모두 `assets:write` 스코프가 필요합니다.

| 방법 | 도구 | 사용 가능한 곳 |
| --- | --- | --- |
| 채팅 안 파일 선택기 | `upload_image_widget`, `upload_audio_widget`, `upload_video_widget` | 웹의 Claude.ai. 이미지 선택기는 파일을 최대 10개까지 받습니다. |
| 브라우저 업로드 페이지 | `request_image_upload`, `request_audio_upload`, `request_video_upload` | 모든 MCP 클라이언트. 도구가 브라우저에서 열 페이지와 파일의 최종 공개 URL을 반환합니다. |
| 미리 서명된 업로드 URL | `prepare_image_upload`, `prepare_audio_upload`, `prepare_video_upload` | Claude Code, Cursor, Cline처럼 셸 명령을 실행할 수 있는 클라이언트. `curl -X PUT --data-binary @file`로 파일을 보내세요. 웹이나 Android의 Claude.ai에서는 쓸 수 없습니다. |

## 자주 묻는 질문

### API로 Nodaro에 파일을 업로드하려면 어떻게 하나요?

file 필드에 파일을 담아 POST /v1/upload를 multipart 폼 데이터로 보내세요. 응답에는 저장된 파일의 url이 들어 있으며, 이 값을 imageUrl, videoUrl, audioUrl로 어떤 노드에든 전달할 수 있습니다.

### 어떤 파일 형식을 업로드할 수 있나요?

이미지는 PNG, JPEG, WebP, 동영상은 MP4, MOV, WebM, 오디오는 MP3, WAV, M4A 또는 AAC, OGG, WebM, FLAC입니다. 오디오 파일은 최대 50MB까지입니다.

### 노드가 파일을 쓰려면 먼저 업로드해야 하나요?

아니요. 대부분의 경로는 공개 http 또는 https URL을 직접 받습니다. URL이 비공개이거나 임시일 때, 또는 Nodaro에 자체 사본을 남기고 싶을 때 파일을 업로드하거나 POST /v1/save-to-storage로 복사하세요.

### 저장 공간이 가득 차면 어떻게 되나요?

업로드가 413으로 응답하고, SDK는 limitBytes에 한도를 담은 StorageExceededError를 발생시킵니다. 더 이상 필요 없는 파일을 삭제하거나 공간을 확보한 다음 다시 업로드하세요.

### 사용자를 Nodaro 라이브러리의 파일 하나로 연결하려면 어떻게 하나요?

https://app.nodaro.ai/library?job= 뒤에 작업 id를 붙이거나, ?asset= 뒤에 GET /v1/library?owned=true의 id를 붙여 링크하세요. 페이지가 파일을 찾아 그 위치로 스크롤하고 미리 보기를 엽니다.

### AI 어시스턴트는 MCP로 파일을 어떻게 업로드하나요?

MCP 서버는 채팅 안의 파일 선택기, 브라우저 업로드 페이지, 셸 명령을 실행할 수 있는 클라이언트를 위한 미리 서명된 업로드 URL의 세 가지 방법을 제공합니다. 각 방법은 이미지, 오디오, 동영상용으로 따로 있습니다.
