> ## 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.

# 주문

> 잠정 주문과 확정 주문, 그리고 취소

편집이 끝난 프로젝트를 주문으로 넘기는 흐름은 **잠정 주문**과 **확정 주문** 두 단계를 거칩니다. 두 단계로 나뉘어 있는 이유는 결제 뒤 생산에 들어가기 전에 사용자가 주문을 취소하거나 결제를 환불할 때 되돌릴 여지를 남기기 위해서입니다.

## 상태 흐름

프로젝트의 `status`는 아래처럼 바뀝니다.

```mermaid theme={null}
stateDiagram-v2
    editing --> ordering: 잠정 주문
    ordering --> ordered: 확정 주문
    ordering --> editing: 취소
    ordered --> rendering
    rendering --> rendered
```

| 단계 | 프로젝트 `status` | 편집 | 취소 | 설명 |
| - | - | - | - | - |
| 편집 중 | `editing` | 가능 | | 사용자가 자유롭게 편집하고 저장합니다 |
| 잠정 주문 | `ordering` | 불가 | 가능 | 주문 의사를 등록한 상태입니다. 편집 권한이 회수됩니다 |
| 확정 주문 | `ordered` | 불가 | 불가 | 주문이 렌더링 대기열에 들어간 상태입니다 |

이후 렌더링 앱이 주문을 가져가면 `rendering`과 `rendered`가 이어집니다. `status`는 열린 문자열이라 이 목록 밖의 값이 올 수도 있습니다. 모르는 값을 오류로 처리하지 마세요.

## 1. 주문 가능한지 확인

사용자가 뒤로가기나 브라우저 강제 종료로 저장하지 못한 경우를 먼저 걸러냅니다.

```ts theme={null}
const { can_order, doc_rev, status } = await callEdicus<CanOrderResult>(
  `/v2/projects/${project_id}/order/can_order`,
)
if (!can_order) throw new Error('저장되지 않은 프로젝트입니다')
```

`callEdicus`는 [Server API 호출하기](/docs/server/client#래퍼-하나-만들기)에서 만든 래퍼이고, `CanOrderResult` 같은 타입은 [응답 타입](/docs/server/client#응답-타입)에서 만든 별칭입니다. 아래 예제도 모두 이 둘을 씁니다.

서버는 문서 리비전이 0이 아닌지(`doc_rev !== 0`)로 판단합니다. `can_order`가 `false`여도 오류가 아니라 200입니다. 리비전이 기록되지 않은 프로젝트는 `doc_rev`가 `null`이고, 이때 `can_order`는 `true`입니다. `status`도 찾지 못하면 `null`입니다.

## 2. 잠정 주문

결제가 완료된 뒤에 호출합니다. 실제 생산에 들어가기 전, 주문 의사를 시스템에 등록하는 단계입니다.

```ts theme={null}
const r = await callEdicus<TentativeOrderResult>(
  `/v2/projects/${project_id}/order/tentative`,
  {
    method: 'POST',
    uid,
    body: { order_count: 1, total_price: 23500, partner_order_id: 'ORD-2026-0001' },
  },
)
// r.order_id는 정수, r.status === 'ordering'
```

* **편집 잠금**: 더 이상 디자인을 수정하거나 저장할 수 없습니다. 데이터 무결성을 위해 편집 권한이 회수됩니다.
* **취소 가능**: 아직 확정 주문이 아니므로 취소하고 `editing`으로 되돌릴 수 있습니다.

<Warning>
  응답으로 받은 `order_id`는 고객사 DB에 반드시 기록하세요. 취소에 이 값이 필요하고, 렌더링 결과와 주문을 맞춰 볼 때도 씁니다.
</Warning>

### 주문 body

<ParamField body="order_count" type="integer" required>
  주문 수량입니다. 1 이상의 정수여야 합니다.
</ParamField>

<ParamField body="total_price" type="number" required>
  실 결제 금액입니다. 0 이상이어야 합니다.
</ParamField>

<ParamField body="partner_order_id" type="string">
  파트너사의 주문 번호입니다. 나중에 이 값으로 주문을 찾을 수 있습니다. 255자 이내여야 합니다.
</ParamField>

<ParamField body="order_name" type="string">
  주문을 구분하는 이름(주문명)입니다. 형식은 정해져 있지 않아 주문자 이름을 넣어도 됩니다. 255자 이내여야 합니다. 렌더링 변수 `$OT`는 `userdata_json`의 `order_name`이 없을 때 이 값을 씁니다.
</ParamField>

<ParamField body="order_for_test" type="boolean" default="false">
  `true`면 렌더링 테스트용 주문입니다. Edicus Prepress의 자동 렌더링(**Start Render**)은 **Render Target**이 `development`일 때 테스트 주문만, `production`일 때 실주문만 가져갑니다. **Manual Render**는 테스트 주문과 실주문을 가리지 않고 주문 번호로 언제든 렌더링합니다. 운영 주문에는 이 필드를 보내지 않거나 `false`로 보냅니다. 자세한 내용은 [테스트 주문과 렌더링](/docs/templates/test-render)에 있습니다.
</ParamField>

<ParamField body="userdata_json" type="string">
  렌더링할 때 쓸 주문 정보를 전달합니다. 객체가 아니라 **JSON 문자열**이어야 하고 **1000자 이내**여야 합니다. 객체를 만든 뒤 `JSON.stringify`로 바꿔 보내세요.

  ```ts theme={null}
  const userdata_json = JSON.stringify({
    user_name: '주문자 이름',
    receiver_name: '수령인 이름',
    order_name: '주문명',
    production_count: 1,
    output_prefix: 'ORD-2026-0001',
    order_detail: { item_name: '명함', option_name: '고급지' },
  })
  ```

  렌더링 앱(Edicus Prepress)이 읽는 키는 아래와 같습니다. 다른 키를 넣어도 오류는 나지 않지만 렌더링에는 쓰이지 않습니다.

  | 키 | 쓰이는 곳 |
  | - | - |
  | `user_name` | 변수 `$UN`입니다. Custom Path를 끈 기본 결과 폴더 이름에도 주문 번호 뒤에 붙습니다 |
  | `receiver_name` | 변수 `$RN`입니다 |
  | `order_name` | 변수 `$OT`입니다. 없으면 주문 body의 `order_name`을 씁니다 |
  | `production_count` | 변수 `$RC`입니다 |
  | `edicus_platform` | 변수 `$PE`입니다 |
  | `market_platform` | 변수 `$PM`입니다. `origin`이 없으면 주문 출처로도 씁니다 |
  | `seller_id` | 변수 `$SN`입니다 |
  | `order_id` | 변수 `$BN`(마켓 주문 번호)입니다. 잠정 주문 응답의 `order_id`와는 다른 값입니다 |
  | `store_item_id` | 변수 `$IN`입니다 |
  | `editor_item_id` | 변수 `$EN`입니다 |
  | `order_detail.item_name`, `order_detail.option_name` | 변수 `$IT`, `$PT`입니다 |
  | `options` | 변수 `$OP`입니다. `색상: 빨강 / 크기: L`처럼 `/`로 나눈 항목마다 `:` 뒤의 값만 꺼내 `-`로 잇습니다(`빨강-L`). 괄호와 그 안의 내용은 지웁니다 |
  | `production_request_datetime` | 변수 `$OD`(YYYYMMDD)의 기준 시각입니다. 프리프레스 설정의 마감 시각(Deadline)을 넘으면 다음 날이 됩니다. [프리프레스 설정](/docs/products/render-output#프리프레스-설정)을 보세요. `2026-10-08T14:30:00+09:00`처럼 날짜로 읽을 수 있는 문자열로 보냅니다 |
  | `order_datetime` | `production_request_datetime`이 없을 때 `$OD`의 기준 시각입니다 |
  | `origin` | 주문 출처입니다. `NSS`(네이버 스마트스토어)면 프리프레스가 NSS용 경로 규칙과 Deadline을 씁니다 |
  | `output_prefix` | 결과 파일 이름 앞에 붙는 접두어입니다 |

  `userdata_json`을 보내면서 `user_name`과 `receiver_name`을 빼면 `$UN`, `$RN`은 `noname`이 됩니다. 변수를 폴더와 파일 이름에 쓰는 방법은 [렌더 출력 옵션](/docs/products/render-output#결과-폴더와-파일-이름-규칙)에 있습니다. 폴더 이름에 쓸 수 없는 문자(`\ / : "` 등)가 들어가면 렌더링이 실패할 수 있습니다.
</ParamField>

규칙을 어기면 `400 invalid_request`이고, `error.details.issues`에 어긋난 필드가 들어 있습니다.

### VDP 주문

가변 데이터를 포함해 주문하려면 잠정 주문 대신 `POST /v2/projects/:project_id/order/tentative_with_vdp`를 씁니다. 보내는 방법과 dataset 형식은 [VDP 주문](/docs/server/orders-vdp)에 있습니다.

## 3. 확정 주문

잠정 주문 상태에서 생산을 개시합니다. 주문이 렌더링 대기열에 들어가고, 렌더링 앱이 가져가면 인쇄용 고해상도 파일(PDF, JPG, PNG 등)을 만듭니다.

```ts theme={null}
const d = await callEdicus<DefinitiveOrderResult>(
  `/v2/projects/${project_id}/order/definitive`,
  { method: 'POST', uid },
)
// d.order_id, d.status === 'ordered'
```

* **취소 불가**: 확정 주문은 취소할 수 없습니다.
* **생산 파일 생성**: 확정 주문이 되어야 렌더링 대상이 됩니다. 잠정 주문만으로는 렌더링되지 않습니다.

생산 파일(PDF 등)은 Server API로 내려받지 않습니다. 파트너가 운영하는 렌더링 앱 Edicus Prepress가 `ordered` 상태의 주문을 가져가 정해 둔 폴더에 파일을 만듭니다. 렌더링이 끝났을 때 내 서버로 알림을 받으려면 프리프레스의 **Callback URL**을 설정합니다. 형식은 [렌더 출력 옵션](/docs/products/render-output#callback-url-형식)에 있습니다.

## 4. 취소

잠정 주문 상태에서만 가능합니다.

```ts theme={null}
const c = await callEdicus<CancelOrderResult>(`/v2/orders/${order_id}/cancel`, {
  method: 'POST',
  uid,
})
// c.order_id는 정수, c.status === 'canceled'
```

응답의 `status`는 주문의 상태인 `canceled`입니다. 같은 때 프로젝트의 `status`는 `editing`으로 돌아가고, 사용자는 편집기를 다시 열어 수정하고 저장할 수 있습니다. 취소한 주문은 지워지므로 같은 `order_id`로 다시 찾을 수 없습니다. 경로의 `order_id`는 양의 정수여야 하고, 아니면 `400 invalid_request`입니다.

## 5. 렌더링 상태 초기화

렌더링이 실패했을 때 재시도를 구현하기 위한 엔드포인트입니다. 주문을 다시 `ordered`로 돌려 렌더링 앱이 다시 가져가게 합니다. Edicus Manager **Order**의 주문 행 **More** 메뉴에 있는 **reorder**와 같습니다.

```ts theme={null}
await callEdicus(`/v2/orders/${order_id}/status/reset_as_ordered`, { method: 'PUT' })
```

`uid`를 요구하지 않습니다. 이 파트너의 주문이 아니거나 없는 주문이면 `404 order_not_found`입니다. 성공하면 빈 객체 `{}`가 옵니다.

## 요약

| 액션 | 엔드포인트 | 전제 조건 | 응답 `status` | 그 뒤 프로젝트 `status` |
| - | - | - | - | - |
| 잠정 주문 | `POST /v2/projects/:project_id/order/tentative` | `can_order === true` | `ordering` | `ordering` |
| VDP 잠정 주문 | `POST /v2/projects/:project_id/order/tentative_with_vdp` | `can_order === true` | `ordering` | `ordering` |
| 확정 | `POST /v2/projects/:project_id/order/definitive` | 현재 `ordering` | `ordered` | `ordered` |
| 취소 | `POST /v2/orders/:order_id/cancel` | 현재 `ordering` | `canceled`(주문) | `editing` |
| 렌더링 상태 초기화 | `PUT /v2/orders/:order_id/status/reset_as_ordered` | 렌더링 재시도 | 없음(`{}`) | 주문이 `ordered`로 돌아갑니다 |

## 전체 예제

```ts theme={null}
import { callEdicus, EdicusError } from './edicus'
import type { CanOrderResult, DefinitiveOrderResult, TentativeOrderResult } from './edicus-types'

// myOrderId는 고객사의 주문 번호입니다
export async function orderProject(project_id: string, uid: string, myOrderId: string) {
  const { can_order } = await callEdicus<CanOrderResult>(
    `/v2/projects/${project_id}/order/can_order`,
  )
  if (!can_order) throw new Error('저장되지 않은 프로젝트입니다')

  try {
    const t = await callEdicus<TentativeOrderResult>(
      `/v2/projects/${project_id}/order/tentative`,
      {
        method: 'POST',
        uid,
        body: { order_count: 1, total_price: 23500, partner_order_id: myOrderId },
      },
    )
    // 취소와 대조에 쓰도록 바로 저장합니다
    await saveEdicusOrderId(myOrderId, t.order_id)

    const d = await callEdicus<DefinitiveOrderResult>(
      `/v2/projects/${project_id}/order/definitive`,
      { method: 'POST', uid },
    )
    return { order_id: d.order_id }
  } catch (e) {
    if (e instanceof EdicusError) console.error(e.endpoint, e.code, e.requestId)
    throw e
  }
}
```

`saveEdicusOrderId`는 고객사 DB에 Edicus 주문 번호를 기록하는 함수입니다. 내 서비스에 맞게 만듭니다.

## 주문이 실패할 때

주문 API의 실패는 **4xx 상태 코드와 body의 `error.code`로 옵니다.**

| 상태 | `error.code` | 언제 | 어떻게 하나 |
| - | - | - | - |
| `400` | `invalid_request` | body나 헤더가 규칙에 맞지 않습니다. `userdata_json`이 JSON이 아니거나 1000자를 넘는 경우도 여기입니다 | `error.details.issues`를 보고 고치세요 |
| `404` | `project_not_found` | `project_id`가 잘못됐거나 삭제됐습니다 | id를 확인하세요 |
| `409` | `project_not_editing` | 이미 주문된 프로젝트입니다. `error.details.status`와 `error.details.order_id`가 함께 옵니다 | 기존 주문을 확인하세요 |
| `409` | `project_locked` | 같은 프로젝트에 주문이 이미 진행 중입니다 | 잠시 후 재시도하세요 |
| `409` | `doc_not_found` | 저장된 문서가 없습니다 | 편집기에서 저장했는지 확인하세요 |

확정 주문은 `404 project_not_found`, `409 project_not_ordering`(잠정 주문 상태가 아님), `409 order_id_missing`을 냅니다.

주문 취소는 `404 order_not_found`, `404 project_not_found`, `403 order_not_owned`(그 사용자의 주문이 아님), `409 project_not_ordering`, `409 order_id_mismatch`를 냅니다. 전체 목록은 [오류 처리](/docs/server/errors)에 있습니다.

## 다음 단계

<Columns cols={2}>
  <Card title="VDP 주문" icon="table" href="/docs/server/orders-vdp">
    가변 데이터를 실어 주문합니다.
  </Card>

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

  <Card title="Server API 레퍼런스" icon="server" href="/docs/api-reference/introduction">
    주문 엔드포인트의 전체 필드를 봅니다.
  </Card>
</Columns>


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