# 커뮤니티 라이브러리

> 커뮤니티 라이브러리에 공유된 캐릭터, 장소, 오브젝트를 REST API로 둘러보고 검색하고 즐겨찾기에 추가하고 복제하며, 게시물을 신고해 검토를 요청합니다.

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

**커뮤니티 라이브러리 API**로 Nodaro 인스턴스에 공유된 캐릭터, 장소, 오브젝트 카탈로그를 둘러보고, 게시물을 내 라이브러리로 복제할 수 있습니다. 카탈로그는 선별해 관리됩니다. 인스턴스 관리자가 게시물을 게시하고, 로그인한 모든 사용자는 게시물을 검색하고, 즐겨찾기에 추가하고, 복제하고, 신고할 수 있습니다.

라이브러리는 다중 사용자 기능입니다. Nodaro Cloud와 셀프 호스팅 Business 에디션에 있으며, Community Edition 인스턴스에서는 이 라우트가 등록되지 않아 `404`를 반환합니다. 인증에는 Bearer 토큰을 사용합니다. 개인 API 토큰(`ndr_…`), OAuth 앱 토큰(`ndr_app_…`), 또는 세션 토큰을 보내세요. [인증](https://nodaro.ai/docs/developers/api/authentication)을 참조하세요.

## 엔드포인트
| 메서드 | 경로 | 설명 |
| --- | --- | --- |
| `GET` | `/v1/community/browse` | 공개 게시물 목록을 한 페이지씩 가져옵니다. |
| `GET` | `/v1/community/detail/:slug` | 슬러그로 게시물 하나를 가져옵니다. |
| `GET` | `/v1/community/favorites` | 내가 즐겨찾기에 추가한 게시물을 가져옵니다. |
| `POST` | `/v1/community/listings/:id/clone` | 게시물을 내 라이브러리로 복사합니다. |
| `POST` | `/v1/community/listings/:id/favorite` | 즐겨찾기에 추가하거나 제거합니다. |
| `POST` | `/v1/community/listings/:id/report` | 게시물을 신고해 검토를 요청합니다. |

## 게시물에 담기는 정보
모든 읽기 라우트는 다음 공개 필드만 담은 게시물을 반환합니다.

| 필드 | 내용 |
| --- | --- |
| `id`, `slug` | 쓰기 라우트에서 쓰는 게시물 id와, 상세 라우트에서 쓰는 슬러그입니다. |
| `entity_type` | `character`, `location`, `object` 중 하나입니다. |
| `title`, `description`, `category`, `style`, `tags` | 관리자가 게시할 때 작성한 내용입니다. |
| `creator_display_name` | 게시물을 게시한 사람입니다. |
| `preview_media_url`, `preview_images` | 미리보기 이미지와 이미지 갤러리입니다. |
| `clone_count`, `favorite_count` | 게시물이 복제된 횟수와 즐겨찾기에 추가된 횟수입니다. |
| `created_at` | 게시물이 게시된 시각입니다. |

## 둘러보기와 검색
`GET /v1/community/browse`는 `{ data: Listing[], nextCursor }`를 반환합니다. 다음 페이지를 가져오려면 `nextCursor`를 `cursor`로 다시 보내세요. 결과가 더 없으면 `nextCursor`는 `null`입니다.

| 쿼리 매개변수 | 설명 |
| --- | --- |
| `entityType` | `character`, `location`, `object` 중 하나입니다. |
| `q` | 제목, 설명, 태그 전체에서 전문 검색을 합니다. |
| `category` | 한 카테고리만 가져옵니다. |
| `sort` | `newest`(기본값) 또는 복제가 많은 순인 `popular`입니다. |
| `limit` | 페이지 크기로, 기본값은 20이고 최대 50입니다. |
| `cursor` | 이전 페이지의 `nextCursor`입니다. |

**curl**

```bash
curl "https://app.nodaro.ai/v1/community/browse?entityType=character&sort=popular&limit=20" \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts

const client = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})

const { data, nextCursor } = await client.community.browse({
entityType: 'character',
sort: 'popular',
limit: 20,
})
```

```json
{
"data": [
{
"id": "e4b2d8f1-6a3c-4e9b-8d7f-1c5a3e9b2d6f",
"entity_type": "character",
"slug": "detective-mara",
"title": "Detective Mara",
"description": "Noir-styled investigator",
"category": "people",
"style": "realistic",
"tags": ["noir", "detective"],
"creator_display_name": "Nodaro Team",
"preview_media_url": "https://cdn.nodaro.ai/community/detective-mara.png",
"clone_count": 128,
"favorite_count": 41,
"created_at": "2026-08-30T14:02:11Z"
}
],
"nextCursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMCJ9"
}
```

`GET /v1/community/detail/:slug`는 `{ data: Listing }`을 반환하며, 게시물이 없거나 내려간 경우에는 `404 not_found`를 반환합니다. `GET /v1/community/favorites`는 `{ data: Listing[] }`을 반환합니다.

## 게시물 복제
`POST /v1/community/listings/:id/clone`은 게시물을 내 라이브러리로 복사하고 `{ entityType, id }`를 반환합니다. 새로 생긴 캐릭터, 장소, 오브젝트의 종류와 id입니다. 본문은 `{ entityType }`이며, 게시물의 종류와 일치해야 합니다.

- **링크가 아니라 복사본입니다.** 게시물의 이미지와 클립이 내 저장 공간으로 복사됩니다. 이후 원본이 변경되거나 게시물이 내려가도 복사본에는 영향이 없습니다.
- **내 것입니다.** 복제본은 일반 캐릭터, 장소, 오브젝트와 같습니다. 이름을 바꾸고, 편집하고, 에셋을 다시 생성하거나 삭제할 수 있습니다.
- **이름이 겹치지 않습니다.** 같은 이름이 이미 있으면 복제본에 복사 접미사가 붙은 고유한 이름이 지정됩니다.
- **내 저장 공간을 사용합니다.** 계정이 저장 공간 한도를 넘으면 복제가 `413 storage_limit_exceeded`로 거부됩니다.

OAuth 앱 토큰으로 복제하려면 `assets:write` 스코프가 필요합니다. 개인 API 토큰에는 스코프가 필요하지 않습니다.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/community/listings/e4b2d8f1-6a3c-4e9b-8d7f-1c5a3e9b2d6f/clone \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "entityType": "character" }'
```

**TypeScript SDK**

```ts
const { id } = await client.community.clone(listingId, 'character')
const mara = await client.characters.get(id)
```

```json
{ "entityType": "character", "id": "5a7c9e1b-3d4f-4a2c-b8e6-9f1d3b5a7c2e" }
```

새 id는 [캐릭터](https://nodaro.ai/docs/developers/api/characters), [장소](https://nodaro.ai/docs/developers/api/locations), [오브젝트](https://nodaro.ai/docs/developers/api/objects) API에서 사용하세요.

## 게시물 즐겨찾기
`POST /v1/community/listings/:id/favorite`은 즐겨찾기를 켜거나 끄고 `{ favorited }`를 반환합니다. 추가하면 `true`, 제거하면 `false`입니다.

```ts
const { favorited } = await client.community.favorite(listingId)
const { data: favorites } = await client.community.favorites()
```

## 게시물 신고
`POST /v1/community/listings/:id/report`는 관리자가 검토하도록 게시물을 신고하고 `{ ok: true }`를 반환합니다. 본문은 `{ reason }`이며, 값은 다음 중 하나입니다.

| `reason` | 게시물이 다음과 같을 때 사용 |
| --- | --- |
| `real_person_no_consent` | 동의하지 않은 실제 인물이 나옵니다. |
| `inappropriate` | 부적절한 콘텐츠가 있습니다. |
| `ip_violation` | 다른 사람의 지식 재산권을 침해합니다. |
| `other` | 그 밖의 문제가 있습니다. |

```ts
await client.community.report(listingId, 'real_person_no_consent')
```

신고 후 게시물이 내려갈 수 있습니다. 사용자가 이미 복제한 복사본은 각자의 라이브러리에 그대로 남습니다.

## 선별 방식의 게시
카탈로그는 인스턴스 관리자가 선별해 관리합니다. 게시물 게시는 공개 API나 SDK에 포함되지 않습니다. [커뮤니티 라이브러리](https://nodaro.ai/docs/guides/community-library)를 참조하세요.

## 오류
| 상태 | 코드 | 의미 |
| --- | --- | --- |
| `400` | `validation_error` | 필드가 없거나 잘못되었습니다. |
| `401` | `unauthorized` | 토큰이 없거나, 잘못되었거나, 취소되었습니다. |
| `403` | `insufficient_scope` | 복제할 때 OAuth 앱 토큰에 `assets:write`가 없습니다. |
| `404` | `not_found` | 게시물이 없거나 내려갔거나, 인스턴스가 Community Edition입니다. |
| `413` | `storage_limit_exceeded` | 계정이 저장 공간 한도를 넘었습니다. |

## 자주 묻는 질문

### 복제한 게시물은 원본과 연결되어 있나요?

아니요. 복제하면 게시물의 이미지와 클립이 내 저장 공간으로 복사됩니다. 복사본은 자유롭게 편집할 수 있으며, 원본이 변경되거나 내려가도 그대로 남습니다.

### API로 내 캐릭터를 커뮤니티 라이브러리에 게시할 수 있나요?

아니요. 카탈로그는 인스턴스 관리자가 선별해 관리합니다. API로는 로그인한 모든 사용자가 게시물을 둘러보고, 즐겨찾기에 추가하고, 복제하고, 신고할 수 있습니다.

### 내 인스턴스에서 커뮤니티 라이브러리 API가 404를 반환하는 이유는 무엇인가요?

라이브러리는 Business 에디션과 Cloud 에디션의 다중 사용자 기능입니다. Community Edition 인스턴스에는 이 라우트가 등록되지 않으므로 모든 호출이 404를 반환합니다.

### 저장 공간이 가득 찬 상태에서 게시물을 복제하면 어떻게 되나요?

복제가 413 storage_limit_exceeded로 거부되며 아무것도 복사되지 않습니다. 저장 공간을 확보한 뒤 다시 시도하세요.
