> ## Documentation Index
> Fetch the complete documentation index at: https://make.edicus.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Server API 호출하기

> 서버에서 Edicus HTTP API를 직접 호출하는 방법

Edicus는 **서버용 JS SDK를 제공하지 않습니다.** 서버 연동은 HTTP 엔드포인트를 직접 호출해서 합니다. 언어와 런타임을 가리지 않고, 새 엔드포인트가 생겨도 패키지 업데이트를 기다릴 필요가 없습니다.

이 페이지는 그 직접 호출을 매번 다시 쓰지 않도록 얇은 래퍼를 하나 만들고, 자주 쓰는 호출을 그 래퍼로 보여 줍니다. 예제는 TypeScript의 `fetch`지만 어느 언어로 옮기셔도 동작은 같습니다.

<Danger>
  Server API는 `edicus-api-key` 헤더를 요구합니다. **반드시 서버에서만 호출하세요.** 이 키 하나로 그 파트너의 모든 사용자와 프로젝트, 주문에 접근할 수 있습니다. 브라우저에는 이 API로 발급받은 [토큰](/docs/concepts/authentication)만 내려보냅니다.
</Danger>

## 호스트가 둘입니다

| 호스트 | 담당 |
| - | - |
| `https://api.edicus.io` | 토큰, 프로젝트, 주문, 폰트 |
| `https://resource-dot-edicusbase.appspot.com` | 상품, 템플릿, 리소스 |

경로 앞이 `/v2`면 앞의 호스트, `/resapi`나 `/manapi`면 뒤의 호스트입니다.

## 공통 헤더

| 헤더 | 언제 | 설명 |
| - | - | - |
| `edicus-api-key` | 항상 | 모션원에서 발급받은 API 키입니다 |
| `edicus-uid` | 사용자 단위 요청 | 고객사가 사용자에게 부여한 고유 id입니다. [형식 규칙](/docs/concepts/authentication#uid)이 있습니다 |
| `edicus-target-uid` | 복제할 때 | 복제본을 받을 사용자입니다. 생략하면 `edicus-uid`와 같습니다 |
| `content-type` | body가 있을 때 | `application/json`. VDP 파일 업로드만 `multipart/form-data`입니다 |

## 래퍼 하나 만들기

매 호출마다 헤더를 붙이고 오류를 확인하는 코드가 반복되므로, 한 번 감싸 두고 쓰시기를 권합니다.

```ts server/edicus.ts theme={null}
const API_BASE = 'https://api.edicus.io'
const RESOURCE_BASE = 'https://resource-dot-edicusbase.appspot.com'

export class EdicusError extends Error {
  constructor(
    readonly endpoint: string,
    readonly httpStatus: number,
    // api.edicus.io의 error.code. 리소스 호스트는 body 모양이 달라 없을 수 있습니다
    readonly code: string | undefined,
    // 문의할 때 함께 알려 주는 값입니다
    readonly requestId: string | undefined,
    readonly body: unknown,
  ) {
    super(`${endpoint} 실패 (${httpStatus}${code ? ` ${code}` : ''})`)
  }
}

type CallOptions = {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  uid?: string
  targetUid?: string
  body?: unknown
  host?: 'api' | 'resource'
}

export async function callEdicus<T>(path: string, opts: CallOptions = {}): Promise<T> {
  const { method = 'GET', uid, targetUid, body, host = 'api' } = opts

  const headers: Record<string, string> = {
    'edicus-api-key': process.env.EDICUS_API_KEY!,
  }
  if (uid) headers['edicus-uid'] = uid
  if (targetUid) headers['edicus-target-uid'] = targetUid
  if (body !== undefined) headers['content-type'] = 'application/json'

  const res = await fetch(`${host === 'api' ? API_BASE : RESOURCE_BASE}${path}`, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  })

  const json = await res.json().catch(() => undefined)

  // 실패는 상태 코드로 옵니다. api.edicus.io는 body가 { error: { code, message, details?, request_id } }입니다
  if (!res.ok) {
    const error = (json as { error?: { code?: string; request_id?: string } } | undefined)?.error
    throw new EdicusError(
      `${method} ${path}`,
      res.status,
      error?.code,
      error?.request_id ?? res.headers.get('x-request-id') ?? undefined,
      json,
    )
  }

  return json as T
}
```

`res.ok`가 아니면 실패입니다. `api.edicus.io`에서는 `error.code`로 원인을 구분하고, 로그에는 `requestId`를 함께 남기세요. 엔드포인트별로 오는 `error.code`와 대응 방법은 [오류 처리](/docs/server/errors)에 정리돼 있습니다.

<Note>
  `AbortSignal.timeout()`은 Node.js 18 이상에서 쓸 수 있습니다. 그보다 낮은 버전이면 `AbortController`와 `setTimeout`으로 직접 만드세요.
</Note>

## 응답 타입

`api.edicus.io`의 요청과 응답 모양은 OpenAPI 문서 하나에 정의돼 있습니다. TypeScript라면 이 문서에서 타입 파일을 만들어 씁니다.

```sh theme={null}
npx openapi-typescript https://api.edicus.io/v2/openapi.json -o edicus-server-api.d.ts
```

만든 파일에서 스키마 이름으로 꺼내 씁니다. 이 페이지와 [주문](/docs/server/orders)의 예제는 아래 별칭을 씁니다.

```ts server/edicus-types.ts theme={null}
import type { components } from './edicus-server-api'

type Schemas = components['schemas']

export type Project = Schemas['Project']
export type ProjectListResult = Schemas['ProjectListResult']
export type PreviewUrlsManyResult = Schemas['PreviewUrlsManyResult']
export type CloneResult = Schemas['CloneResult']
export type CloneAsyncResult = Schemas['CloneAsyncResult']
export type CanOrderResult = Schemas['CanOrderResult']
export type TentativeOrderResult = Schemas['TentativeOrderResult']
export type DefinitiveOrderResult = Schemas['DefinitiveOrderResult']
export type CancelOrderResult = Schemas['CancelOrderResult']
```

다른 언어는 각자의 OpenAPI 생성기를 쓰면 됩니다. 리소스 호스트(`/resapi`, `/manapi`)는 이 문서에 들어 있지 않으므로 필요한 필드만 직접 타입으로 적습니다.

<Note>
  `@edicus/sdk/types`는 편집기 메시지와 띄우기 옵션의 타입만 내보냅니다. Server API의 응답 타입은 들어 있지 않습니다.
</Note>

## uid는 호출할 때마다 넘깁니다

사용자 단위 요청은 `edicus-uid` 헤더가 없거나 [형식 규칙](/docs/concepts/authentication#uid)을 어기면 `400 invalid_request`입니다. 이 헤더를 클라이언트 수준의 기본값으로 고정해 두면 다른 사용자의 프로젝트를 건드리게 되므로, 로그인한 사용자의 `uid`를 호출할 때마다 넘기시는 편이 안전합니다.

```ts theme={null}
const project = await callEdicus<Project>(`/v2/projects/${project_id}`, {
  uid: user.edicusUid,
})
```

어느 엔드포인트가 `edicus-uid`를 요구하는지는 [Server API 레퍼런스](/docs/api-reference/introduction)의 각 페이지에 표시돼 있습니다.

## 엔드포인트 한눈에 보기

| 용도 | 엔드포인트 | 호스트 |
| - | - | - |
| 토큰 발급 | `POST /v2/auth/token` | api |
| 프로젝트 조회 | `GET /v2/projects/:project_id` | api |
| 프로젝트 목록 | `GET /v2/projects` | api |
| 소유자 조회 | `GET /v2/projects/:project_id/owner` | api |
| 프리뷰 URL | `GET /v2/projects/:project_id/preview_urls` | api |
| 프리뷰 URL(여러 개) | `POST /v2/projects/preview_urls` | api |
| 프로젝트 삭제 | `DELETE /v2/projects/:project_id` | api |
| 복제 | `POST /v2/projects/:project_id/clone` | api |
| 비동기 복제 | `POST /v2/projects/:project_id/clone_async` | api |
| 주문 가능 여부 | `GET /v2/projects/:project_id/order/can_order` | api |
| 잠정 주문 | `POST /v2/projects/:project_id/order/tentative` | api |
| 잠정 주문(VDP) | `POST /v2/projects/:project_id/order/tentative_with_vdp` | api |
| 확정 주문 | `POST /v2/projects/:project_id/order/definitive` | api |
| 주문 취소 | `POST /v2/orders/:order_id/cancel` | api |
| 렌더링 상태 초기화 | `PUT /v2/orders/:order_id/status/reset_as_ordered` | api |
| 폰트 그룹 id 목록 | `GET /v2/font/group_id_list` | api |
| 폰트 목록 | `GET /v2/font/group/:font_group_id/list` | api |
| 상품 목록 | `GET /resapi/product/list` | resource |
| 상품 조회 | `GET /resapi/product/:product_id` | resource |
| 리소스 검색 | `POST /resapi/query` | resource |
| 리소스 개수 | `POST /resapi/query/count` | resource |

각 엔드포인트의 요청과 응답 필드는 [Server API 레퍼런스](/docs/api-reference/introduction)에서 확인하세요.

## 프로젝트 다루기

```ts theme={null}
const project = await callEdicus<Project>(`/v2/projects/${project_id}`, { uid })
// project.status로 편집 가능한지 판단합니다
```

없는 `project_id`이거나 그 사용자의 프로젝트가 아니면 `404 project_not_found`입니다.

목록 조회의 응답은 `{ projects: [...] }`로 감싸여 있습니다. 프로젝트가 없으면 빈 배열입니다.

```ts theme={null}
const { projects } = await callEdicus<ProjectListResult>('/v2/projects', { uid })
```

### 프리뷰 썸네일

```ts theme={null}
const { urls } = await callEdicus<{ urls: string[] }>(
  `/v2/projects/${project_id}/preview_urls`,
)
```

프리뷰가 없으면 `urls`가 빈 배열입니다.

여러 프로젝트를 한 번에 조회할 때는 `POST /v2/projects/preview_urls`를 씁니다. **한 번에 최대 25개**이고, 넘기면 `400 invalid_request`입니다. 요청 전에 직접 끊어 보내세요.

```ts theme={null}
const result = await callEdicus<PreviewUrlsManyResult>('/v2/projects/preview_urls', {
  method: 'POST',
  body: { project_ids: ids.slice(0, 25) },
})

for (const item of result.results) {
  if (item.error) console.warn(item.project_id, item.error)
  else renderThumbnails(item.urls ?? [])
}
```

<Note>
  항목별 실패는 요청 전체의 실패가 아닙니다. 응답은 200이고, 실패한 항목만 `results[i].error`(예: `no_preview_urls`)에 남으므로 직접 확인하셔야 합니다.
</Note>

### 복제

```ts theme={null}
const { project_id: copy, preview_tns } = await callEdicus<CloneResult>(
  `/v2/projects/${project_id}/clone`,
  { method: 'POST', uid, targetUid },
)
```

응답에는 복제본의 id와 프리뷰 썸네일 목록(`preview_tns`, 각 항목에 `url`)이 함께 옵니다. 복제본의 제목에는 `(복제됨)`이 붙습니다. 포토북처럼 이미지가 많으면 수십 초 이상 걸려 타임아웃이 날 수 있으니 그런 경우 비동기 복제를 씁니다.

```ts theme={null}
const { project_id: copy } = await callEdicus<CloneAsyncResult>(
  `/v2/projects/${project_id}/clone_async`,
  {
    method: 'POST',
    uid,
    body: { callback_url: 'https://partner.example.com/edicus/clone-done' },
  },
)
```

비동기 복제는 `202 Accepted`로 새 id를 즉시 반환하고 복제는 서버에서 계속됩니다. 그동안 프로젝트 상태는 `cloning`이고, 끝나면 `editing`이 됩니다. `callback_url`을 주면 끝났을 때 그 주소로 결과를 POST합니다. 콜백 body와 재시도 규칙은 [비동기 복제 레퍼런스](/docs/api-reference/projects/clone-project-async)에 있습니다.

<Warning>
  `cloning` 상태인 프로젝트를 편집기로 열면 로드에 실패합니다. 프로젝트를 조회해 상태를 확인한 뒤 여세요.
</Warning>

<Warning>
  **같은 원본에 복제 요청이 동시에 들어와도 서버가 막지 않습니다.** 중복 요청을 막아야 한다면 호출하는 쪽에서 직접 처리하셔야 합니다.
</Warning>

복제가 실패하면 원본이 없을 때 `404 project_not_found`, `template_uri`가 없는 프로젝트일 때 `409 project_not_clonable`입니다.

### 삭제

```ts theme={null}
await callEdicus(`/v2/projects/${project_id}`, { method: 'DELETE', uid })
```

`editing` 상태인 프로젝트만 삭제할 수 있고, 주문된 프로젝트는 `409 project_not_editing`입니다. 주문 완료된 프로젝트는 보관 기간(3\~4주)이 지나면 자동으로 삭제됩니다. 성공하면 빈 객체 `{}`가 옵니다.

## 리소스 호스트 호출하기

상품과 리소스 엔드포인트는 호스트가 다릅니다. `host: 'resource'`를 주면 됩니다.

```ts theme={null}
// 리소스 호스트는 OpenAPI 문서에 없으므로 쓰는 필드만 직접 적습니다
type ProductList = { prodCates: unknown[]; products: { prodCode: string; dpName: string }[] }

const { products } = await callEdicus<ProductList>('/resapi/product/list', {
  host: 'resource',
})
```

<Warning>
  리소스 호스트는 오류 body의 모양이 다릅니다. `error` 객체가 없고, `400`은 JSON 문자열 하나(예: `"no option"`), `401`은 `{"code": ...}`, `500`은 빈 객체 `{}`인 경우가 많습니다. 위 래퍼는 `res.ok`로 실패를 잡지만 `code`와 `requestId`는 비어 있을 수 있습니다. 자세한 차이는 [오류 처리](/docs/server/errors)를 보세요.
</Warning>

## 다음 단계

<Columns cols={2}>
  <Card title="주문" icon="cart-shopping" href="/docs/server/orders">
    잠정 주문과 확정 주문을 넣습니다.
  </Card>

  <Card title="오류 처리" icon="triangle-exclamation" href="/docs/server/errors">
    상태 코드와 `error.code`로 실패를 다룹니다.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.