# Sweetbook API 개발문서 — 전체 본문 > 포토북 PDF 생성·주문·배송 자동화를 위한 REST API. > Base URL: https://api.sweetbook.com (Live) / https://api-sandbox.sweetbook.com (Sandbox). > 본 파일은 모든 페이지 본문을 합친 단일 마크다운입니다. 페이지별 인덱스는 /docs/llms.txt 참조. --- # Book Print API 개요 Book Print API를 사용하면 포토북 생성부터 주문, 배송까지 전 과정을 자동화할 수 있습니다. 파트너 여정: 가입 → Sandbox 테스트 → 사업 협의 → 가격 결정 → Live 운영 ## 기본 정보 > ℹ️ **모든 API 요청에는 API Key가 필요합니다.** 파트너 포털에서 API Key를 발급받아 요청 헤더에 포함하세요. > 자세한 내용은 [인증 가이드](/docs/authentication)를 참조하세요. **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books' \\ -H "Authorization: Bearer {YOUR_API_KEY}" ``` ## 표준 응답 형식 모든 API는 다음과 같은 표준 응답 형식을 사용합니다. ### 성공 응답 > ℹ️ 성공 응답에서는 `errors` 필드가 JSON에 포함되지 않습니다. **Response** ```json { "success": true, "message": "Success", "data": { ... } } ``` ### 실패 응답 요청이 실패하면 success가 false이며, errors 배열에 상세 오류 메시지가 포함됩니다. **Error response** ```json { "success": false, "message": "Error message", "data": null, "errors": ["세부 에러 메시지들"] } ``` ## 시작하기 처음 연동을 시작하시는 경우 아래 순서대로 진행하세요. 각 API 레퍼런스는 좌측 사이드바에서 확인할 수 있습니다. - [**파트너 등록**](/docs/registration) — 가입하고 Sandbox API Key를 발급받으세요. - [**인증 (API Key)**](/docs/authentication) — 발급받은 API Key로 요청을 인증하는 방법을 확인하세요. - [**창작 방식 선택**](/docs/concepts/creation-types) — PDF 업로드 / 템플릿 / 혼합 중 적합한 방식을 결정하세요. - [**전체 워크플로우**](/docs/guides/workflow) — 책 생성부터 주문·배송까지 전체 흐름을 따라가세요. - [**환경 (Sandbox / Live)**](/docs/environments) — Sandbox와 Live 환경 차이와 도메인 분리 방식을 알아보세요. - [**SDKs & Demo Apps**](/docs/sdks-and-demos) — Python · Node.js SDK와 실행 가능한 데모 웹앱으로 빠르게 시작하세요. --- # Book Print API 개요 Book Print API를 사용하면 포토북 생성부터 주문, 배송까지 전 과정을 자동화할 수 있습니다. 파트너 여정: 가입 → Sandbox 테스트 → 사업 협의 → 가격 결정 → Live 운영 ## 기본 정보 > ℹ️ **모든 API 요청에는 API Key가 필요합니다.** 파트너 포털에서 API Key를 발급받아 요청 헤더에 포함하세요. > 자세한 내용은 [인증 가이드](/docs/authentication)를 참조하세요. **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books' \\ -H "Authorization: Bearer {YOUR_API_KEY}" ``` ## 표준 응답 형식 모든 API는 다음과 같은 표준 응답 형식을 사용합니다. ### 성공 응답 > ℹ️ 성공 응답에서는 `errors` 필드가 JSON에 포함되지 않습니다. **Response** ```json { "success": true, "message": "Success", "data": { ... } } ``` ### 실패 응답 요청이 실패하면 success가 false이며, errors 배열에 상세 오류 메시지가 포함됩니다. **Error response** ```json { "success": false, "message": "Error message", "data": null, "errors": ["세부 에러 메시지들"] } ``` ## 시작하기 처음 연동을 시작하시는 경우 아래 순서대로 진행하세요. 각 API 레퍼런스는 좌측 사이드바에서 확인할 수 있습니다. - [**파트너 등록**](/docs/registration) — 가입하고 Sandbox API Key를 발급받으세요. - [**인증 (API Key)**](/docs/authentication) — 발급받은 API Key로 요청을 인증하는 방법을 확인하세요. - [**창작 방식 선택**](/docs/concepts/creation-types) — PDF 업로드 / 템플릿 / 혼합 중 적합한 방식을 결정하세요. - [**전체 워크플로우**](/docs/guides/workflow) — 책 생성부터 주문·배송까지 전체 흐름을 따라가세요. - [**환경 (Sandbox / Live)**](/docs/environments) — Sandbox와 Live 환경 차이와 도메인 분리 방식을 알아보세요. - [**SDKs & Demo Apps**](/docs/sdks-and-demos) — Python · Node.js SDK와 실행 가능한 데모 웹앱으로 빠르게 시작하세요. --- # 파트너 등록 가이드 가입부터 Sandbox API Key 발급까지의 절차를 안내합니다. 5분이면 완료할 수 있습니다. ## 전체 흐름 ## Step 1. 회원가입 1. 파트너 포털 가입 페이지에 접속합니다 2. 이메일 주소를 입력하고 **인증번호 발송**을 클릭합니다 3. 이메일로 수신된 인증번호를 입력합니다 4. 비밀번호를 설정하고 가입을 완료합니다 > ℹ️ 가입 즉시 **Personal 계정**이 생성되며, Sandbox 환경을 사용할 수 있습니다. Live 환경은 스위트북과 사업 협의 후 사용 가능합니다. ## Step 2. 파트너 포털 로그인 1. 파트너 포털 로그인 페이지에서 가입한 이메일과 비밀번호로 로그인합니다 2. 로그인 후 파트너 대시보드가 표시됩니다 ## Step 3. Sandbox API Key 발급 1. **파트너 포털 > 설정 > API Key**를 클릭합니다 2. **새 API Key 발급** 버튼을 클릭합니다 3. 환경에서 **Sandbox**를 선택합니다 4. 메모(선택)를 입력하고 발급합니다 5. 발급된 API Key를 복사하여 안전한 곳에 저장합니다 > ⚠️ **API Key는 발급 시에만 전체 값을 확인할 수 있습니다.** 이후에는 접두사(prefix)만 표시됩니다. 발급 즉시 안전한 곳에 저장하세요. - 모든 API Key는 `SB` 접두사로 시작합니다 - 접두사와 시크릿은 `.`으로 구분됩니다 - 발급 개수 제한이 없으며, 필요 시 폐기 후 재발급 가능합니다 - 만료 기간 없이 폐기하지 않는 한 계속 유효합니다 **API Key format** ```text SB{12자리 prefix}.{시크릿} 예시: SB9A1X36H2YZ.a1b2c3d4e5f6g7h8i9j0... ``` ## Step 4. 첫 API 호출 테스트 발급받은 Sandbox API Key로 상품 목록을 조회해보세요. > ℹ️ 응답이 정상적으로 돌아왔다면 연동 준비가 완료된 것입니다. ### 인증 실패 시 | 에러 | 원인 | 해결 | | --- | --- | --- | | `401 Unauthorized` | API Key가 잘못되었거나, Sandbox Key로 Live URL에 요청 | API Key 확인, Base URL이 `api-sandbox`인지 확인 | | `403 Forbidden` | 폐기된 API Key 사용, 또는 IP 제한에 걸림 | 파트너 포털에서 API Key 상태 확인 | **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/book-specs' \\ -H "Authorization: Bearer {YOUR_API_KEY}" ``` **Response** ```json { "success": true, "message": "Success", "data": [ { "bookSpecUid": "PHOTOBOOK_A4_SC", "name": "A4 소프트커버 포토북", "pageMin": 24, "pageMax": 130, ... } ] } ``` ## Sandbox 충전금 - **파트너 포털 > 충전금 > 충전** 메뉴에서 원하는 금액을 충전할 수 있습니다 - Sandbox 주문 시 테스트 가격(100원 이하)이 적용됩니다 - Sandbox 충전금과 Live 충전금은 완전히 분리되어 있습니다 ## 다음 단계 - [**인증 (API Key)**](/docs/authentication) — 발급받은 API Key로 요청을 인증하는 방법을 확인하세요. - [**환경 (Sandbox / Live)**](/docs/environments) — Sandbox와 Live 환경의 차이와 도메인 구분을 알아보세요. - [**창작 방식 선택**](/docs/concepts/creation-types) — PDF 업로드 / 템플릿 / 혼합 중 적합한 방식을 결정하세요. - [**전체 워크플로우**](/docs/guides/workflow) — 책 생성부터 주문·배송까지 전체 흐름을 따라가세요. --- # 환경 (Sandbox / Live) Book Print API는 **Sandbox**와 **Live** 두 가지 환경을 제공합니다. 각 환경은 완전히 분리되어 있으며, API Key와 Base URL이 다릅니다. ## API Base URL > ⚠️ **Sandbox와 Live의 Base URL이 다릅니다.** 환경이 맞지 않을 때 반환되는 응답은 두 가지로 구분됩니다. > > - **환경 불일치 Key (403 `ERR_ENV_MISMATCH`)** — Sandbox 전용 Key로 Live URL을 호출하거나 반대의 경우 > - **Personal 계정의 Live 호출 (403)** — Business 계정 전환 전까지는 Live 환경 사용 불가 > > > 응답 본문 포맷은 [에러 코드 & 트러블슈팅](/docs/operations/errors) 문서를 참고하세요. | 환경 | Base URL | | --- | --- | | **Sandbox** | `https://api-sandbox.sweetbook.com/v1` | | **Live** | `https://api.sweetbook.com/v1` | 서버는 요청 도메인을 기반으로 환경을 자동 판별합니다. 별도의 환경 파라미터가 필요하지 않습니다. **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/book-specs' \\ -H "Authorization: Bearer {YOUR_API_KEY}" ``` ## 환경별 차이 | 항목 | Sandbox | Live | | --- | --- | --- | | Base URL | `api-sandbox.sweetbook.com` | `api.sweetbook.com` | | API Key | Sandbox 전용 키 | Live 전용 키 | | 충전금 | Sandbox 충전금 (파트너 포털에서 충전) | 실제 충전금 (결제로 충전) | | 가격 | 테스트 가격 (`sandboxPriceBase` + `sandboxPricePerIncrement` × 페이지) | 협의된 실제 가격 (`priceBase`/`pricePerIncrement`) | | 실제 인쇄 | 하지 않음 | 실제 인쇄 진행 | | 실제 배송 | 하지 않음 | 지정 택배사 배송 (평균 3~4 영업일 출고) | | 웹훅 | 발생함 | 발생함 | | 주문 상태 | 결제완료(PAID)에서 멈춤 (이후 전이 없음) | 결제완료 → 제작 → 출고 → 배송완료 전체 흐름 진행 | | 데이터 | Sandbox 전용 (Live와 완전 분리) | Live 전용 | ## 계정 타입별 환경 접근 가입 시 Personal 계정이 생성되며, 사업 협의 완료 후 Business 계정으로 전환됩니다. | 계정 타입 | Sandbox | Live | 전환 조건 | | --- | --- | --- | --- | | **Personal** | 사용 가능 | 사용 불가 | 가입 즉시 | | **Business** | 사용 가능 | 사용 가능 | 스위트북과 사업 협의 완료 후 | ## Sandbox 환경 상세 ### Sandbox 충전금 - **파트너 포털 > 충전금 > 충전** 메뉴에서 Sandbox 충전금을 충전할 수 있습니다 - Sandbox 충전금을 원하는 금액만큼 충전할 수 있습니다 - Sandbox 충전금과 Live 충전금은 완전히 분리되어 있습니다 ### 테스트 가격 Sandbox 환경의 `GET /book-specs` 응답은 실제 가격 필드(`priceBase`, `pricePerIncrement`)를 `null`로 숨기고, 별도의 `sandboxPriceBase`와 `sandboxPricePerIncrement` 필드로 테스트 가격을 제공합니다. 예: `sandboxPriceBase=100`, `sandboxPricePerIncrement=10`, 페이지 26 → 기본 가격 **100 + (26-24) × 10 = 120원**. 값은 상품(BookSpec)에 따라 달라질 수 있습니다. ### 테스트 주문 - 주문 생성 시 Sandbox 충전금이 차감됩니다 - 주문 상태가 **결제완료(PAID)**에서 멈춥니다 (실제 제작/배송이 진행되지 않음) - 웹훅 이벤트는 정상적으로 발생합니다 - 주문 전체 취소 · 항목별 부분 취소 모두 테스트할 수 있습니다 > ℹ️ Sandbox 환경에서 생성한 책, 주문, 웹훅 설정 등의 데이터는 Live 환경으로 이관되지 않습니다. ## Live 환경으로 전환하기 1. **사업 협의** — 스위트북과 개별 협의를 진행합니다 (가격, 상품, 운영 조건 등) 2. **Business 계정 전환** — 협의 완료 후 계정이 Business로 전환됩니다 3. **Live API Key 발급** — 파트너 포털에서 Live 환경 API Key를 발급합니다 4. **실제 충전금 충전** — 파트너 포털에서 결제를 통해 충전금을 충전합니다 5. **코드 변경** — Base URL과 API Key만 변경하면 됩니다. API 인터페이스는 동일합니다 **Configuration** ```bash BASE_URL=https://api-sandbox.sweetbook.com/v1 API_KEY={YOUR_API_KEY} ``` 위 탭을 Live로 전환하면 `BASE_URL`이 운영 도메인으로 자동 치환됩니다. 실제 운영 전환 시에는 `API_KEY`도 Live 환경에서 발급받은 값으로 교체해야 합니다. ## 다음 단계 - [**인증 (API Key)**](/docs/authentication) — 환경별 API Key 사용 방법과 인증 헤더 형식을 확인하세요. - [**창작 방식 선택**](/docs/concepts/creation-types) — PDF 업로드 / 템플릿 / 혼합 중 적합한 방식을 결정하세요. - [**전체 워크플로우**](/docs/guides/workflow) — 책 생성부터 주문·배송까지 전체 흐름을 따라가세요. ## 관련 문서 - [환경 매트릭스 — — 엔드포인트별 Sandbox / Live 호출 가능 여부와 환경 격리 분류(A·B·C·D)](/docs/operations/environment-matrix) --- # 인증 (API Key) API Key를 사용하여 Book Print API에 인증하는 방법을 설명합니다. ## 개요 Book Print API는 **API Key 기반 Bearer Token 인증**을 사용합니다. 모든 API 요청에 `Authorization` 헤더를 포함해야 합니다. ### API Key 특징 - **SB 접두사**: 모든 API Key는 `SB`로 시작하며 `.`을 포함합니다 - **환경 구분**: Sandbox 키와 Live 키가 분리되어 있습니다 - **IP 제한**: 선택적으로 허용 IP를 설정하여 보안을 강화할 수 있습니다 - **만료 없음**: API Key는 폐기하지 않는 한 계속 유효합니다 **Authenticated request** ```text Authorization: Bearer {YOUR_API_KEY} ``` ## API Key 발급 API Key는 **파트너 포털**에서 발급받을 수 있습니다. 1. 파트너 포털에 로그인합니다 2. **파트너 포털 > 설정 > API Key** 메뉴로 이동합니다 3. **새 API Key 발급** 버튼을 클릭합니다 4. 환경(Sandbox/Live)을 선택합니다 5. 발급된 API Key를 안전하게 저장합니다 > ⚠️ **API Key는 발급 시에만 전체 값을 확인할 수 있습니다.** 발급 후에는 접두사(prefix)만 표시되므로, 발급 즉시 안전한 곳에 저장하세요. ### 환경별 API Key | 환경 | 용도 | 과금 | | --- | --- | --- | | `sandbox` | 개발 및 테스트 환경 | Sandbox 충전금 사용 | | `live` | 프로덕션 환경 (사업 협의 후 사용 가능) | 실제 충전금 차감 | ## Sandbox 환경 환경에 따라 API Base URL이 다르며, 서버는 요청 도메인을 기반으로 자동으로 환경을 판별합니다. ### 도메인 기반 환경 분기 - **Sandbox**: `https://api-sandbox.sweetbook.com` - **Live**: `https://api.sweetbook.com` > ⚠️ **Sandbox API Key는 반드시 Sandbox 도메인으로 호출해야 합니다.** Live 도메인으로 호출하면 환경 불일치 에러(403 `ERR_ENV_MISMATCH`)가 반환됩니다. **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books' \\ -H "Authorization: Bearer {YOUR_API_KEY}" ``` ## JavaScript (fetch) 아래 코드블록의 탭으로 Sandbox / Live 도메인을 전환할 수 있습니다. **JavaScript (fetch)** ```javascript const API_KEY = '{YOUR_API_KEY}'; const BASE_URL = 'https://api-sandbox.sweetbook.com/v1'; const response = await fetch(\`\${BASE_URL}/books\`, { headers: { 'Authorization': \`Bearer \${API_KEY}\` } }); const data = await response.json(); // { "success": true, "data": [...], "message": "Success" } ``` ## JavaScript (axios) **JavaScript (axios)** ```javascript import axios from 'axios'; const api = axios.create({ baseURL: 'https://api-sandbox.sweetbook.com/v1', headers: { 'Authorization': 'Bearer {YOUR_API_KEY}' } }); const { data } = await api.get('/books'); console.log(data); ``` ## Python (requests) **Python** ```python import requests API_KEY = '{YOUR_API_KEY}' BASE_URL = 'https://api-sandbox.sweetbook.com/v1' headers = { 'Authorization': f'Bearer {API_KEY}' } response = requests.get(f'{BASE_URL}/books', headers=headers) data = response.json() print(data) ``` ## 인증 에러 | 상태 코드 | 의미 | 원인 | | --- | --- | --- | | 401 Unauthorized | 인증 실패 (`ERR_UNAUTHORIZED`) | `Authorization` 헤더 누락, Bearer 토큰 무효/만료 | | 403 Forbidden | 접근 거부 | API Key가 폐기되었거나 허용 IP에 의해 차단됨. **Sandbox Key로 Live 호출(또는 반대) — 환경 불일치 시 `ERR_ENV_MISMATCH`**. **Personal 계정이 Live 도메인을 호출한 경우에도 403** 반환 | | 429 Too Many Requests | 요청 제한 초과 | 요청 빈도 제한 초과 — `Retry-After` 헤더 값만큼 대기 후 재시도 | **401 응답은 표준 6필드 JSON 본문**으로 반환됩니다. 다른 errorCode와 응답 포맷 상세는 [에러 코드 & 트러블슈팅](/docs/operations/errors)를 참고하세요. **401 Unauthorized (ERR_UNAUTHORIZED)** ```json { "success": false, "errorCode": "ERR_UNAUTHORIZED", "message": "Unauthorized", "data": null, "errors": ["인증이 필요합니다"], "fieldErrors": [] } ``` **Error handling** ```javascript const response = await fetch('https://api-sandbox.sweetbook.com/v1/books', { headers: { 'Authorization': \`Bearer \${API_KEY}\` } }); if (!response.ok) { switch (response.status) { case 401: { const error = await response.json(); console.error('인증 실패 — Authorization 헤더 또는 API Key를 확인하세요.', error.errors?.[0] ?? error.message); break; } case 403: { const error = await response.json(); // ERR_ENV_MISMATCH: Sandbox/Live 도메인과 Key 환경 불일치 console.error('접근 거부:', error.errorCode ?? '', error.errors?.[0] ?? error.message); break; } case 429: { const retryAfter = response.headers.get('Retry-After'); console.error(\`요청 제한 초과 — \${retryAfter}초 후 재시도\`); break; } default: { const error = await response.json(); console.error('API 에러:', error.errors?.[0] ?? error.message); } } } ``` ## API Key 보안 API Key는 형식이며, `.` 뒤의 Secret 부분은 발급 시 1회만 전달됩니다. - **Secret은 발급 시 1회만 표시**: 서버는 원본 Secret을 보관하지 않으므로 분실 시 재확인 불가 — 발급 즉시 안전한 곳에 저장하세요. - **허용 IP 설정 (`allowIps`)**: 파트너 포털 UI에서 쉼표로 구분된 IP 목록(최대 10개)을 등록하면 해당 IP에서만 Key 사용을 허용합니다. - **환경 변수 사용**: API Key를 코드/저장소에 직접 포함하지 말고 환경 변수로 주입하세요. - **클라이언트 측 노출 금지**: 반드시 서버 사이드에서만 호출하고, 브라우저/모바일 앱에는 절대 포함하지 마세요. > ℹ️ Rate Limiting에 대한 자세한 내용은 [Rate Limiting 가이드](/docs/operations/rate-limiting)를 참고하세요. **.env** ```bash # .env SWEETBOOK_API_KEY={YOUR_API_KEY} ``` **Node.js** ```javascript // Node.js에서 환경 변수 사용 const API_KEY = process.env.SWEETBOOK_API_KEY; const response = await fetch('https://api-sandbox.sweetbook.com/v1/books', { headers: { 'Authorization': \`Bearer \${API_KEY}\` } }); ``` ## FAQ ### Q: Sandbox 키와 Live 키의 차이는 무엇인가요? Sandbox 키로 생성한 책과 주문은 실제 인쇄/배송이 진행되지 않고 결제완료(PAID) 상태에서 멈춥니다. 주문 금액은 Sandbox 전용 충전금에서 차감되며 실제 돈이 빠져나가지 않습니다. ### Q: API Key가 노출되면 어떻게 하나요? 즉시 파트너 포털에서 해당 API Key를 폐기하고 새로운 키를 발급받으세요. ### Q: Postman에서 테스트하려면 어떻게 하나요? Authorization 탭에서 Type을 **Bearer Token**으로 선택하고 API Key를 입력하세요. --- # 전체 워크플로우 Book Print API는 두 가지 방식으로 포토북을 제작할 수 있습니다. 파트너 환경에 맞는 방식을 선택하세요. ## 방식 선택 | 항목 | PDF 업로드 | 템플릿 기반 | 혼합 (표지 템플릿 + 내지 PDF) | | --- | --- | --- | --- | | creationType | `PDF_UPLOAD` | `TEMPLATE` | `MIX_COVER_TEMPLATE` | | 적합한 경우 | 이미 PDF를 직접 제작하는 파트너 | 제공되는 템플릿으로 포토북을 구성하는 파트너 | 표지 디자인은 템플릿 활용, 내지는 자체 PDF 제작 | | 표지 입력 | PDF 업로드 (`/pdf-cover`) | 템플릿 (`/cover`) | 템플릿 (`/cover`) | | 내지 입력 | PDF 업로드 (`/pdf-contents`) | 템플릿 (`/contents`) | PDF 업로드 (`/pdf-contents`) | | 책 최종화 | 별도 최종화 API 호출 필요 | 별도 최종화 API 호출 필요 | 별도 최종화 API 호출 필요 | | 주문 직후 상태 | 즉시 `PDF_READY` | `PAID` → 렌더링 후 `PDF_READY` | `PAID` → 렌더링 후 `PDF_READY` | | 상세 가이드 | [PDF 업로드 시나리오](/docs/guides/scenario-pdf) | [템플릿 기반 시나리오](/docs/guides/scenario-template) | [혼합 방식 시나리오](/docs/guides/scenario-mix) | ## PDF 업로드 워크플로우 이미 제작된 PDF를 업로드하여 포토북을 만드는 방식입니다. → )} 1. **책 생성** — `creationType="PDF_UPLOAD"` + `pageCount` 지정 2. **표지 업로드** — 표지 PDF 파일 업로드 (크기 자동 검증). POST=신규(409 if exists) / PUT=교체(404 if missing) 3. **내지 업로드** — 내지 PDF 파일 업로드 (페이지 수 검증). POST/PUT 동일 규약 4. **책 최종화** — 표지·내지 PDF 업로드를 마친 뒤 호출. 책이 FINALIZED 상태로 전환되어 주문 가능 5. **주문 생성** — 주문 생성 및 충전금 차감 6. **상태 추적** — 주문 상태 변경 알림 수신 ### 시퀀스 다이어그램 ```mermaid sequenceDiagram autonumber participant P as 파트너 서버 participant S as Book Print API P->>S: POST /books (creationType=PDF_UPLOAD, pageCount) S-->>P: bookUid 반환 (DRAFT) P->>S: POST /books/{bookUid}/pdf-cover S-->>P: 표지 검증 결과 (DRAFT 유지) P->>S: POST /books/{bookUid}/pdf-contents S-->>P: 내지 검증 결과 (DRAFT 유지) P->>S: POST /books/{bookUid}/finalization S-->>P: 최종화 완료 (FINALIZED) P->>S: POST /orders S-->>P: 주문 응답 (충전금 차감) S-->>P: Webhook (order.created, shipping.departed, ...) ``` > ℹ️ 단계별 curl 예시와 응답은 [PDF 업로드 시나리오](/docs/guides/scenario-pdf)에서 확인하세요. ## 템플릿 기반 워크플로우 제공되는 템플릿에 텍스트와 이미지를 전달하여 포토북을 구성하는 방식입니다. → )} 1. **책 생성** — DRAFT 상태의 빈 책 생성 2. **표지 추가** — 표지 템플릿 적용 + 이미지/텍스트 바인딩 3. **내지 추가** — 내지 템플릿 적용. 반복 호출하여 페이지 추가 4. **책 최종화** — DRAFT → FINALIZED. 페이지 수 규칙 검증 5. **주문 생성** — 주문 생성 및 충전금 차감 6. **상태 추적** — 주문 상태 변경 알림 수신 ### 시퀀스 다이어그램 ```mermaid sequenceDiagram autonumber participant P as 파트너 서버 participant S as Book Print API P->>S: POST /books S-->>P: bookUid 반환 (DRAFT) P->>S: POST /books/{bookUid}/cover S-->>P: 표지 적용 완료 loop 내지 추가 (반복) P->>S: POST /books/{bookUid}/contents S-->>P: 내지 적용 완료 end P->>S: POST /books/{bookUid}/finalization S-->>P: 최종화 완료 (FINALIZED) P->>S: POST /orders S-->>P: 주문 응답 (충전금 차감) S-->>P: Webhook (order.created, shipping.departed, ...) ``` > ℹ️ 단계별 curl 예시와 응답은 [템플릿 기반 시나리오](/docs/guides/scenario-template)에서 확인하세요. ## 혼합 방식 워크플로우 표지는 템플릿으로 적용하고 내지는 PDF로 직접 업로드하는 방식입니다. → )} 1. **책 생성** — `creationType="MIX_COVER_TEMPLATE"` + `pageCount` 지정 2. **표지 템플릿 적용** — 표지 템플릿 + 이미지/텍스트 바인딩 (PDF 표지는 차단) 3. **내지 PDF 업로드** — 내지 PDF 파일 업로드 (POST=신규, PUT=교체). 템플릿 내지는 차단 4. **책 최종화** — 표지·내지 입력 후 호출. 책이 FINALIZED 상태로 전환되어 주문 가능 5. **주문 생성** — 주문 생성 및 충전금 차감. 표지 렌더링 후 PDF_READY로 전환 6. **상태 추적** — 주문 상태 변경 알림 수신 ### 시퀀스 다이어그램 ```mermaid sequenceDiagram autonumber participant P as 파트너 서버 participant S as Book Print API P->>S: POST /books (creationType=MIX_COVER_TEMPLATE, pageCount) S-->>P: bookUid 반환 (DRAFT) P->>S: POST /books/{bookUid}/cover S-->>P: 표지 템플릿 적용 (DRAFT 유지) P->>S: POST /books/{bookUid}/pdf-contents S-->>P: 내지 PDF 검증 결과 (DRAFT 유지) P->>S: POST /books/{bookUid}/finalization S-->>P: 최종화 완료 (FINALIZED) P->>S: POST /orders S-->>P: 주문 응답 (충전금 차감) S-->>P: Webhook (order.created, shipping.departed, ...) ``` > ℹ️ 단계별 curl 예시와 응답은 [혼합 방식 시나리오](/docs/guides/scenario-mix)에서 확인하세요. ## 핵심 제약 조건 ### 책 상태 규칙 - **DRAFT**: 표지/내지 추가, 사진 업로드 가능. 편집은 이 상태에서만 가능. - **FINALIZED**: 최종화 완료. 더 이상 편집 불가. 주문은 이 상태에서만 가능. ### 페이지 수 규칙 최종화 시 다음 조건을 모두 만족해야 합니다: - `actualPageCount >= pageMin` — 실제 페이지 수가 최소 페이지 수 이상 - `actualPageCount <= pageMax` — 실제 페이지 수가 최대 페이지 수 이하 - `(actualPageCount - pageMin) % pageIncrement == 0` — 최소 페이지 수에서 증분 단위씩 증가한 값이어야 함 `pageMin`/`pageMax`/`pageIncrement`는 판형 조회(`GET /book-specs`) 응답에서 확인할 수 있습니다. ### 표지 규칙 - 책 1권당 표지는 1개만 허용 (중복 추가 시 에러) - DRAFT 상태에서만 추가 가능 ### 인증 - 모든 API 호출에 API Key 인증 필수 ## 이미지 전달 방식 (템플릿 기반) 템플릿 기반 방식에서 표지/내지 추가 시 이미지를 전달하는 방법은 3가지입니다: | 방식 | 설명 | 사전 업로드 필요 | | --- | --- | --- | | 파일 업로드 | multipart/form-data로 파일 직접 전송 (`$upload` 플레이스홀더) | 불필요 | | URL | 공개 접근 가능한 이미지 URL 지정 | 불필요 | | 서버 파일명 | `POST /books/{bookUid}/photos`로 사전 업로드한 파일의 서버 파일명 참조 | **필요** | ## 활용 시나리오 ### PDF 업로드형 (인쇄 대행) 이미 편집 도구로 PDF를 제작하는 파트너가 인쇄/배송만 위탁하는 시나리오입니다. - 파트너가 PDF 크기와 페이지 수를 직접 관리 - 표지·내지 PDF 업로드 후 `POST /books/{bookUid}/finalization` 호출로 FINALIZED 상태 전환 - 별도 템플릿 조회 불필요 (PDF로 최종 레이아웃 완성) [PDF 업로드 시나리오 상세 가이드 →](/docs/guides/scenario-pdf) ### 사용자 선택형 (앨범 앱) 사용자가 직접 사진을 선택하고 포토북을 만드는 시나리오입니다. 앨범 앱, 사진 편집 앱 등에 적합합니다. - 사용자의 로컬 사진을 사용하므로 `POST /books/{bookUid}/photos`로 사전 업로드 후 서버 파일명을 참조하는 방식 사용 - 판형/템플릿 목록은 자주 변경되지 않으므로 캐시 권장 - 사용자가 사진 수를 자유롭게 선택하므로 내지 추가 횟수가 가변적 > ℹ️ 참고: [구글포토북 데모앱](https://github.com/sweet-book/socialBook-demo) [템플릿 기반 시나리오 상세 가이드 →](/docs/guides/scenario-template) **호출 패턴 (socialBook-demo 기준)** ```javascript const result = await client.books.create({ title, bookSpecUid: 'SQUAREBOOK_HC' }); const bookUid = result.bookUid; for (const file of localFiles) { await client.photos.upload(bookUid, file); } await client.covers.create(bookUid, templateUid, { frontPhoto, backPhoto, dateRange }); for (const entry of entries) { await client.contents.insert(bookUid, templateUid, { photos: entry.photos }, { breakBefore: 'page' }); } ``` ### 서버 자동 생성형 (일기장/알림장 앱) 서버가 데이터(일기, 알림장 등)를 기반으로 자동으로 포토북을 생성하는 시나리오입니다. 사용자는 기간만 선택하면 서버가 나머지를 처리합니다. - 사진이 이미 서버에 URL로 존재하므로 사전 업로드 불필요 (URL로 직접 전달) - 템플릿 UID와 파라미터 매핑을 서버에 미리 설정 - 간지(챕터 구분) 페이지, 발행면 등 특수 페이지도 `POST /books/{bookUid}/contents`로 추가 > ℹ️ 참고: [일기장 데모앱](https://github.com/sweet-book/diaryBook-demo), [알림장 데모앱](https://github.com/sweet-book/kidsDailyBook-demo) [템플릿 기반 시나리오 상세 가이드 →](/docs/guides/scenario-template) **호출 패턴 (diaryBook-demo 기준)** ```javascript const result = await client.books.create({ title: bookTitle, bookSpecUid: 'SQUAREBOOK_HC' }); const bookUid = result.bookUid; await client.covers.create(bookUid, tplCover, { coverPhoto: photoUrl, title, dateRange }); for (const entry of entries) { if (entry.type === 'ganji') { await client.contents.insert(bookUid, tplGanji, { chapterNum, year, monthTitle }, { breakBefore: 'page' }); } else if (entry.type === 'naeji_a') { await client.contents.insert(bookUid, tplNaejiA, { monthNum, dayNum, diaryText, photo: photoUrl }, { breakBefore: 'page' }); } else if (entry.type === 'naeji_gallery') { await client.contents.insert(bookUid, tplGallery, { collagePhotos: [url1, url2, '...'] }, { breakBefore: 'none' }); } } await client.contents.insert(bookUid, tplPublish, { photo, title, publishDate, author, hashtags }, { breakBefore: 'page' }); ``` ### 혼합형 (템플릿 표지 + PDF 내지) 표지 디자인은 SweetBook이 제공하는 테마별 표지 템플릿을 활용하고, 내지는 파트너 자체 편집 도구에서 만든 PDF를 업로드하는 시나리오입니다. - 표지는 템플릿(`POST /books/{bookUid}/cover`)으로만 적용 — PDF 표지는 차단 - 내지는 PDF(`POST /books/{bookUid}/pdf-contents`)로만 업로드 — 템플릿 내지는 차단 - 표지·내지 입력 후 `POST /books/{bookUid}/finalization`으로 최종화 - 주문 흐름은 TEMPLATE과 동일 (`PAID` → 렌더링 후 `PDF_READY`) [혼합 방식 시나리오 상세 가이드 →](/docs/guides/scenario-mix) ## 관련 문서 - [PDF 업로드 시나리오 — — 기본 권장. 책 생성·표지·내지·최종화·주문 단계별 안내](/docs/guides/scenario-pdf) - [템플릿 기반 시나리오 — — 사진·텍스트 입력으로 책 구성](/docs/guides/scenario-template) - [혼합 방식 시나리오 — — 표지=템플릿, 내지=PDF](/docs/guides/scenario-mix) - [Orders API — — 주문 생성·취소·조회](/docs/api/orders) - [Credits API — — 충전금 잔액·거래 내역 조회](/docs/api/credits) - [Webhook Events — — 9개 이벤트별 페이로드 구조와 예시](/docs/api/webhook-events) --- # SDKs & Demo Apps Book Print API와 연동하기 위한 공식 SDK와 실행 가능한 데모 웹앱을 제공합니다. ## SDKs 서버 환경에서 Book Print API를 호출하기 위한 공식 클라이언트 라이브러리입니다. - [**Python SDK**](https://github.com/sweet-book/bookprintapi-python-sdk) — 포토북 생성부터 주문/배송까지 전체 API를 지원하는 Python SDK입니다. pip install -e .으로 설치하거나 소스를 직접 임포트하여 사용할 수 있습니다. - [**Node.js SDK**](https://github.com/sweet-book/bookprintapi-nodejs-sdk) — Node.js 서버 환경에서 사용할 수 있는 SDK입니다. Express, Fastify 등과 함께 서버 사이드 연동에 적합합니다. ## Demo Apps 실행 가능한 웹앱 예시입니다. `node server.js`로 바로 실행할 수 있습니다. - [**일기장 (diaryBook)**](https://github.com/sweet-book/diaryBook-demo) — 일기장 A/B 타입의 포토북을 생성하는 웹앱입니다. JSON 데이터 업로드 또는 직접 입력으로 책을 만들 수 있습니다. - [**알림장 (kidsDailyBook)**](https://github.com/sweet-book/kidsDailyBook-demo) — 어린이집 알림장 A/B/C 타입의 포토북을 생성하는 웹앱입니다. 월별 색상/캐릭터/풍선 디자인이 자동 적용됩니다. - [**구글포토북 (socialBook)**](https://github.com/sweet-book/socialBook-demo) — Google Photos 연동으로 포토북을 생성하는 웹앱입니다. Google OAuth 로그인 후 사진을 선택하면 자동으로 책이 만들어집니다. - [**주문 (partner-order)**](https://github.com/sweet-book/partner-order-demo) — 파트너 주문 시스템 웹앱입니다. 충전금 관리, 견적 조회, 주문 생성, 배송지 변경, 주문 취소까지 전체 주문 플로우를 체험할 수 있습니다. --- # AI 에이전트 지원 개요 Cursor·Claude Code·Codex·ChatGPT 등 AI 도구가 Book Print API 문서를 정확히 읽고 코드 생성에 활용하도록 3가지 마크다운 산출물을 제공합니다. ## 왜 만들었는가 HTML 문서 페이지는 사람을 위한 것입니다. 사이드바·아이콘·CSS 등이 AI에게는 노이즈가 됩니다. AI 도구가 정확한 코드를 생성하려면 본문만 깨끗한 마크다운으로 받을 수 있어야 합니다. 본 문서는 3가지 표준 산출물(`llms.txt` · `llms-full.txt` · `.md` URL)을 제공하여 **어떤 AI 도구든 URL fetch만으로** 본 문서를 컨텍스트로 사용할 수 있도록 설계되었습니다. llmstxt.org 표준을 따르며, Resend·Stripe 등 주요 API 문서와 같은 패턴입니다. ## 3가지 산출물 | 산출물 | URL | 용도 | | --- | --- | --- | | **llms.txt** (인덱스) | `/docs/llms.txt` | 전체 문서의 페이지 목록. AI 도구가 "어떤 페이지가 있는지" 파악 | | **llms-full.txt** (합본) | `/docs/llms-full.txt` | 모든 페이지 본문을 합친 단일 마크다운. 큰 컨텍스트 윈도우 도구에서 통째로 적재 | | **.md URL** (페이지별) | `/docs/{slug}.md` | 특정 페이지만 마크다운으로. URL 끝에 `.md`만 붙이면 됨 | 세 산출물은 같은 변환기에서 생성되어 일관된 결과를 보장합니다. 페이지가 수정되면 다음 빌드에서 세 곳 모두 자동으로 갱신됩니다. ## 도구별 사용 흐름 모든 AI 도구가 표준 텍스트 파일·URL을 fetch하는 방식이라 별도 설치·플러그인이 필요 없습니다. ### Cursor (IDE) 채팅에 `@`로 URL을 추가하면 자동으로 fetch됩니다. **Cursor 채팅** ```text @https://api.sweetbook.com/docs/llms-full.txt Sandbox 환경에서 PDF 업로드 시나리오대로 Node.js 코드 짜줘. ``` ### Claude Code (CLI) URL을 채팅에 언급하면 `WebFetch` 도구가 자동 호출됩니다. **Claude Code 세션** ```text @https://api.sweetbook.com/docs/llms-full.txt 읽고 주문 부분 취소 클라이언트 코드 작성해줘. ``` ### Codex (OpenAI CLI) `curl`로 받아 컨텍스트로 첨부하거나, 세션 내 URL 언급으로 fetch합니다. **Codex 사용 예** ```bash # 1. 문서 다운로드 curl https://api.sweetbook.com/docs/llms-full.txt > docs.md # 2. Codex 세션에서 docs.md를 컨텍스트로 첨부 codex ``` ### 그 외 도구 - **ChatGPT / Claude.ai 웹**: URL을 채팅에 붙여넣으면 자동 fetch - **GitHub Copilot Chat**: `#fetch` 도구 또는 인라인 URL - **Perplexity / Gemini**: 사용자가 URL 첨부 또는 인라인 입력 - **로컬 RAG 시스템**: `curl`로 다운로드 → 임베딩 인덱싱 ## AI가 자동으로 받는 가드 메모 일부 페이지는 마크다운 응답에 **AI 전용 가드 메모**가 HTML 주석으로 포함됩니다. 사람이 HTML 페이지로 볼 때는 표시되지 않고, AI가 `.md` URL로 fetch할 때만 노출됩니다. **.md 응답 예시 일부** ```markdown # Webhooks API 웹훅을 설정하여 주문 상태 변경 등의 이벤트를 실시간으로 수신하는 방법을 안내합니다. ... ``` 가드 메모는 AI가 자주 헷갈리는 포인트(예: "Bearer 접두사 필수", "충전금 표기 통일", "orderStatus는 문자열 enum")를 명시적으로 알려주어 환각·실수를 줄입니다. 자세한 내용은 [.md URL 사용법](/docs/ai-agents/md-url) 참조. ## 관련 문서 - [llms.txt · llms-full.txt 사용법 — — 인덱스와 전체 합본 비교, 도구별 등록 방법](/docs/ai-agents/llms-txt) - [.md URL 사용법 — — 페이지별 마크다운 fetch + AGENT INSTRUCTIONS 가드 메모](/docs/ai-agents/md-url) --- # llms.txt · llms-full.txt 사용법 AI 에이전트가 Book Print API 전체 문서를 한 번에 읽도록 제공하는 두 가지 산출물의 차이와 사용법입니다. > ℹ️ **바로 확인** > > - [https://api.sweetbook.com/docs/llms.txt ↗](/docs/llms.txt) > - [https://api.sweetbook.com/docs/llms-full.txt ↗](/docs/llms-full.txt) ## llms.txt vs llms-full.txt | 항목 | llms.txt (인덱스) | llms-full.txt (합본) | | --- | --- | --- | | URL | `/docs/llms.txt` | `/docs/llms-full.txt` | | 크기 | ~3 KB (페이지 링크 목록) | ~450 KB (36개 페이지 본문 전체) | | 구조 | 그룹별 페이지 목록 + 각 페이지 `.md` 링크 | 모든 페이지를 `---` 구분자로 연결한 단일 마크다운 | | 토큰 수 | ~1 K | ~30~50 K (한국어 + 영어 코드 혼합) | | 주 용도 | AI가 "어떤 페이지를 더 읽을지" 결정하는 디렉토리 | 큰 컨텍스트 윈도우(128K+) 도구에서 전체 본문 한 번에 적재 | > ℹ️ **먼저 시도해볼 것은 llms-full.txt** — 대부분의 최신 AI 도구(Claude Code 1M·Cursor Pro·ChatGPT Plus 등)는 450 KB > 를 한 번에 적재할 수 있습니다. 토큰 한도가 작은 도구에서만 llms.txt + 페이지별 `.md`를 조합하세요. ## llms.txt 형식 llmstxt.org 표준 형식을 따릅니다. 페이지 링크는 모두 `.md` 확장자로 지정되어 AI가 인덱스 → 페이지 단위 fetch 흐름으로 사용 가능합니다. **llms.txt 응답 예시 (앞부분)** ```text # Sweetbook API 개발문서 > 포토북 PDF 생성·주문·배송 자동화를 위한 REST API. > Base URL: https://api.sweetbook.com (Live) / https://api-sandbox.sweetbook.com (Sandbox). > 인증: Bearer API Key. 충전금 결제 모델. ## 시작하기 - [개요](https://api.sweetbook.com/docs.md) - [파트너 등록](https://api.sweetbook.com/docs/registration.md) - [환경 (Sandbox / Live)](https://api.sweetbook.com/docs/environments.md) - [인증 (API Key)](https://api.sweetbook.com/docs/authentication.md) ## 빠른 시작 ... ``` ## llms-full.txt 형식 모든 페이지 본문을 `---` 구분자로 연결한 단일 마크다운입니다. 페이지 순서는 사이드바 순서를 따릅니다. **llms-full.txt 응답 예시 (앞부분)** ```text # Sweetbook API 개발문서 — 전체 본문 > 포토북 PDF 생성·주문·배송 자동화를 위한 REST API. > Base URL: https://api.sweetbook.com (Live) / https://api-sandbox.sweetbook.com (Sandbox). > 본 파일은 모든 페이지 본문을 합친 단일 마크다운입니다. 페이지별 인덱스는 /docs/llms.txt 참조. --- # 개요 ... --- # 파트너 등록 ... (... 이하 36개 페이지 본문) ``` ## 도구별 등록 방법 ### Cursor 채팅에 `@` + URL 형식으로 첨부합니다. **Cursor 채팅 예시** ```text @https://api.sweetbook.com/docs/llms-full.txt 다음 시나리오대로 코드 짜줘: 1. Sandbox API Key로 인증 2. PDF 업로드 방식으로 책 생성 (creationType=PDF_UPLOAD) 3. 표지·내지 PDF 업로드 후 POST /books/{bookUid}/finalization 명시 호출 4. 주문 생성 후 PDF_READY 상태 확인 ``` 팁: 첨부 후 **"위 문서 기준으로"**를 명시하면 AI가 외부 추측을 줄이고 본문 사실에 더 충실해집니다. ### Claude Code (CLI) 세션 내 URL 언급으로 자동 `WebFetch`됩니다. **Claude Code 사용 예** ```text # 세션 시작 후 > https://api.sweetbook.com/docs/llms-full.txt 읽고 > Sweetbook 충전금 잔액 모니터링 + 자동 충전 스크립트 작성해줘. ``` ### Codex (OpenAI) `curl`로 다운로드 후 세션에 첨부합니다. **Codex 사용 예** ```bash # 1. 합본 다운로드 curl https://api.sweetbook.com/docs/llms-full.txt > sweetbook-docs.md # 2. Codex 세션 시작 (sweetbook-docs.md를 컨텍스트로) codex ``` ### ChatGPT / Claude.ai 웹 URL을 채팅에 직접 붙여넣으면 도구가 자동으로 fetch합니다. 또는 다운로드한 파일을 첨부 모드로 업로드. ### 로컬 RAG 시스템 **RAG 인덱싱 워크플로우** ```bash # 1. 다운로드 curl https://api.sweetbook.com/docs/llms-full.txt > sweetbook.md # 2. 페이지 단위로 분할 (--- 구분자 기준) csplit sweetbook.md '/^---$/' '{*}' # 3. 각 페이지를 임베딩 → 벡터 DB 인덱싱 # (구현은 사용 중인 RAG 프레임워크에 맞춰) ``` ## 자주 묻는 질문 ### Q: 왜 llms.txt와 llms-full.txt를 둘 다 제공하나요? 토큰 한도가 작은 도구나, AI가 "필요한 페이지만 골라 읽기"를 하고 싶을 때는 llms.txt(인덱스) + 페이지별 `.md`가 효율적입니다. 반대로 큰 작업을 한 번에 시킬 때는 llms-full.txt를 통째로 적재하면 캐시 활용도가 높아 후속 질의가 빨라집니다. ### Q: 문서가 업데이트되면 자동으로 반영되나요? 네. 두 산출물 모두 빌드 시점에 페이지 소스에서 자동 생성되므로, 페이지가 갱신되면 다음 배포에서 같이 반영됩니다. AI 도구는 매번 fresh URL을 fetch하므로 별도 캐시 무효화가 필요 없습니다. ### Q: llms.txt 표준은 어디서 확인할 수 있나요? [llmstxt.org ↗](https://llmstxt.org)에서 공식 표준을 확인할 수 있습니다. Resend·Stripe 등 주요 API 문서가 같은 표준을 채택하고 있습니다. ## 관련 문서 - [AI 에이전트 지원 개요 — — 3가지 산출물 한눈에 보기](/docs/ai-agents/overview) - [.md URL 사용법 — — 페이지별 마크다운 + AGENT INSTRUCTIONS](/docs/ai-agents/md-url) --- # .md URL 사용법 특정 페이지만 마크다운으로 받고 싶을 때 사용합니다. URL 끝에 `.md`만 붙이면 같은 페이지를 깨끗한 마크다운으로 응답합니다. ## 기본 사용법 기존 페이지 URL 끝에 `.md` 확장자를 추가합니다. 36개 페이지 각각에 대해 1:1로 마크다운 버전이 매칭됩니다. | HTML URL | .md URL | | --- | --- | | `https://api.sweetbook.com/docs/api/orders` | `https://api.sweetbook.com/docs/api/orders.md` | | `https://api.sweetbook.com/docs/guides/scenario-pdf` | `https://api.sweetbook.com/docs/guides/scenario-pdf.md` | | `https://api.sweetbook.com/docs/authentication` | `https://api.sweetbook.com/docs/authentication.md` | **curl로 직접 확인** ```bash curl https://api.sweetbook.com/docs/api/orders.md ``` ## 주요 사용 케이스 - **특정 페이지만 필요할 때** — 전체 합본(llms-full.txt) 적재가 부담스럽거나, 한 페이지 내용만으로 충분한 작업 - **토큰 한도가 작은 도구** — 모델 컨텍스트 윈도우가 작은 환경에서는 페이지별 fetch가 효율적 - **문서 복사·공유** — 응답을 그대로 노션·이슈·README에 붙여넣기 좋음 - **RAG 시스템** — 페이지 단위 임베딩 인덱싱 ## 도구별 사용 예시 ### Cursor — 특정 페이지 컨텍스트 첨부 **Cursor 채팅** ```text @https://api.sweetbook.com/docs/api/orders.md 이 문서 보고 주문 생성 + 부분 취소 처리 클라이언트 코드만 짜줘. ``` ### 개발자가 직접 URL 조작 HTML 페이지를 읽다가 URL 끝에 `.md`만 추가하고 엔터를 누르면 같은 페이지가 마크다운으로 표시됩니다. 카피해서 다른 도구에 붙여넣기 좋습니다. ### 스크립트 자동화 **여러 페이지 일괄 다운로드** ```bash #!/bin/bash PAGES=("api/orders" "api/webhooks" "guides/scenario-pdf") for slug in "$\{PAGES[@]\}"; do curl -s "https://api.sweetbook.com/docs/$\{slug\}.md" \\ -o "docs/$\{slug//\\//-\}.md" done ``` ## AGENT INSTRUCTIONS — AI 가드 메모 일부 페이지는 `.md` 응답 상단에 **AI 전용 가드 메모**가 HTML 주석으로 포함됩니다. HTML 페이지에서는 표시되지 않고, `.md` URL로 fetch할 때만 노출됩니다. ### 응답 모양 **GET /docs/api/webhooks.md 응답 일부** ```markdown # Webhooks API 웹훅을 설정하여 주문 상태 변경 등의 이벤트를 실시간으로 수신하는 방법을 안내합니다. ... ``` ### 왜 이런 방식인가 - **사람 본문 노이즈 제거** — "Bearer 접두사 필수" 같은 메모는 본문에서 충분히 설명되어 있으므로 중복. 하지만 AI에게는 명시적 가드가 환각·실수를 줄이는 데 효과적이라 마크다운 응답에만 끼워 넣음. - **HTML 페이지 깔끔함 유지** — 사람이 페이지를 읽을 때는 가드 메모가 보이지 않아 시각적 노이즈 없음. - **자동 전파** — [llms-full.txt](/docs/ai-agents/llms-txt) 합본에도 그대로 포함되어 별도 작업 없이 한 번에 노출. ### 가드 메모가 포함된 페이지 현재 9개 페이지에 가드 메모가 적용되어 있습니다. 페이지 본문에서 충분히 설명되지 않은 미세한 규칙이나 AI가 자주 헷갈리는 포인트만 콕 짚어 보강합니다. | 페이지 | 주요 가드 메모 요약 | | --- | --- | | [Orders API](/docs/api/orders) | orderStatus 문자열 enum 11종, 취소 가능 상태, 부분 취소 규칙 | | [Webhooks API](/docs/api/webhooks) | whsk_ 접두사, HMAC-SHA256, 헤더 4종, 재시도 정책 | | [Webhook Events](/docs/api/webhook-events) | envelope snake_case, 이벤트 9종 카탈로그, 전송 상태 | | [Credits API](/docs/api/credits) | "충전금" 표기 통일, KRW 고정, direction +/-, Sandbox 미지원 | | [인증 (API Key)](/docs/authentication) | Bearer 접두사 필수, SB Key 형식, ERR_ENV_MISMATCH | | [템플릿 기반 시나리오](/docs/guides/scenario-template) | creationType TEMPLATE 필수, 흐름, multipart parameters JSON 문자열 | | [PDF 업로드 시나리오](/docs/guides/scenario-pdf) | creationType PDF_UPLOAD 필수, POST/PUT 분리, 500MB 제한 | | [에러 코드 & 트러블슈팅](/docs/operations/errors) | HTTP 아닌 errorCode로 분기, 6필드 응답 shape, constraint 6종 | | [환경 매트릭스](/docs/operations/environment-matrix) | 4가지 분류 A/B/C/D, ERR_SANDBOX_UNSUPPORTED 재시도 제외 | ## 관련 문서 - [AI 에이전트 지원 개요 — — 3가지 산출물 한눈에 보기](/docs/ai-agents/overview) - [llms.txt · llms-full.txt 사용법 — — 인덱스와 전체 합본](/docs/ai-agents/llms-txt) --- # 창작 방식 SweetBook은 책 제작에 3가지 창작 방식(`creationType`)을 제공합니다. 본 페이지는 각 방식의 차이·트레이드오프·선택 기준을 안내합니다. > ℹ️ **기본 권장 방식은 `PDF_UPLOAD`**입니다. `TEMPLATE`·`MIX_COVER_TEMPLATE`은 특정 시나리오에 한정된 보조 옵션입니다 (아래 **의사결정 가이드** 참조). > ℹ️ 방식별 단계별 호출 흐름은 시나리오 페이지([PDF 업로드](/docs/guides/scenario-pdf) · [템플릿 기반](/docs/guides/scenario-template) · [혼합](/docs/guides/scenario-mix)), 엔드포인트 명세는 API 페이지([Books — PDF 기반](/docs/api/books-pdf) · [Books — 템플릿 기반](/docs/api/books-template))를 참고하세요. ## 한눈에 비교 각 방식의 핵심 차이를 정리한 표입니다. 첫 컬럼은 기본 권장 방식 `PDF_UPLOAD`입니다. | 항목 | **PDF_UPLOAD** (기본) | TEMPLATE | MIX_COVER_TEMPLATE | | --- | --- | --- | --- | | **표지 입력** | PDF 직접 업로드 (`POST /books/{uid}/pdf-cover`) | 템플릿 (`POST /books/{uid}/cover`) | 템플릿 (`POST /books/{uid}/cover`) | | **내지 입력** | PDF 직접 업로드 (`POST /books/{uid}/pdf-contents`) | 템플릿 (`POST /books/{uid}/contents`) | PDF 직접 업로드 (`POST /books/{uid}/pdf-contents`) | | **책 최종화** | `POST /books/{uid}/finalization` 명시 호출 | 동일 (명시 호출) | 동일 (명시 호출) | | **주문 직후 상태** | **즉시 `PDF_READY`** (PAID 단계 건너뜀) | `PAID` → 렌더링 후 `PDF_READY` | `PAID` → 표지 렌더링 후 `PDF_READY` | | **디자인 자유도** | 높음 (PDF 페이지 단위 완전 자유) | 낮음 (제공 템플릿 카탈로그 한정) | 표지 한정 (템플릿)·내지 자유 (PDF) | | **`pageCount` 사전 고지** | 필수 (책 생성 시) | 불필요 | 필수 (내지 PDF 페이지 수) | | **BookSpec PDF 검증** | 크기·페이지·시그니처 검증 | 적용 안 함 | 내지 PDF에만 적용 | | **템플릿 바인딩 검증** | 해당 없음 | 필수 파라미터·갤러리 등 검증 | 표지에만 적용 | | **사진 업로드 API** (`/photos`) | **차단** (`ERR_CREATION_TYPE_UNSUPPORTED`) | 지원 | 지원 | | **파트너 사전 작업** | PDF 생성·검증 환경 필요 | 사진·텍스트 입력만 | 표지 입력 + 내지 PDF 생성 | | **상세 가이드** | [PDF 업로드 시나리오](/docs/guides/scenario-pdf) | [템플릿 기반 시나리오](/docs/guides/scenario-template) | [혼합 방식 시나리오](/docs/guides/scenario-mix) | ## 의사결정 가이드 기본은 `PDF_UPLOAD`입니다. 다음 조건에 해당하면 다른 방식을 고려하세요. | 방식 | 선택 기준 | | --- | --- | | **`PDF_UPLOAD`** (기본) | 아래 조건 중 **하나라도** 해당하면 권장: - 자체 디자인 도구·DTP 환경에서 PDF를 생성하는 파트너 - 책 페이지 단위 완전 자유 디자인이 필요 - 주문 직후 즉시 제작 단계로 진행하고 싶음 (렌더링 대기 회피) | | `TEMPLATE` | PDF 생성 환경이 없고, 사진·텍스트 입력 UI로 책을 구성하는 일반 사용자 앱. 템플릿 카탈로그 제약을 수용 가능한 경우. | | `MIX_COVER_TEMPLATE` | 표지는 SweetBook 템플릿으로 브랜드 일관성을 보장하면서 내지는 직접 PDF로 디자인하고 싶은 경우 (시리즈물·교재 등). | ## 각 방식 상세 ### PDF_UPLOAD (기본) 표지·내지 PDF를 직접 업로드해 책을 구성하는 방식입니다. 1. **책 생성:** `POST /books` with `creationType: "PDF_UPLOAD"` + `pageCount`(내지 페이지 수, 필수). 책의 PDF 검증 기준(크기·페이지·시그니처)은 [BookSpecs API](/docs/api/book-specs)의 응답 필드를 참고하세요. 2. **표지 PDF 업로드:** `POST /books/{uid}/pdf-cover` (신규) / `PUT /books/{uid}/pdf-cover` (교체). 3. **내지 PDF 업로드:** `POST /books/{uid}/pdf-contents` (신규) / `PUT /books/{uid}/pdf-contents` (교체). 4. **책 최종화:** `POST /books/{uid}/finalization`. 표지·내지 PDF 둘 다 업로드된 상태여야 finalize 성공. 5. **주문:** `POST /orders`. **주문 생성 시점에 표지·내지 PDF가 모두 준비되어 있으므로** 주문 상태가 `PAID`를 거치지 않고 **즉시 `PDF_READY`로 승격**됩니다. 상세 호출 흐름·요청/응답 예시는 [PDF 업로드 시나리오](/docs/guides/scenario-pdf)를, 엔드포인트 명세는 [Books — PDF 기반](/docs/api/books-pdf)을 참고하세요. ### TEMPLATE 제공된 템플릿에 사진·텍스트 등 파라미터를 바인딩해 책을 구성하는 방식입니다. 1. **책 생성:** `POST /books` with `creationType: "TEMPLATE"`. `pageCount` 불필요 (콘텐츠 추가에 따라 자동 결정). 2. **(선택) 사진 사전 업로드:** `POST /books/{uid}/photos`로 사진을 미리 업로드 후 콘텐츠 추가 시 참조. 3. **표지 추가:** `POST /books/{uid}/cover`로 표지 템플릿 + 파라미터 바인딩. 4. **콘텐츠(내지) 추가:** `POST /books/{uid}/contents`로 콘텐츠 템플릿 + 파라미터 바인딩 (반복 호출). 5. **책 최종화:** `POST /books/{uid}/finalization`. 책의 `pdfStatus`가 `1`(PENDING, 렌더링 대기)로 전환됩니다. 6. **주문:** `POST /orders`. 주문 상태는 `PAID`로 시작하며, 백그라운드 렌더링 완료 후 `PDF_READY`로 전환됩니다. 상세 호출 흐름은 [템플릿 기반 시나리오](/docs/guides/scenario-template)를, 엔드포인트 명세는 [Books — 템플릿 기반](/docs/api/books-template)을, 템플릿 도메인 개념(구조·바인딩·갤러리·베이스 레이어)은 [템플릿 구조와 바인딩](/docs/concepts/template-structure)을 참고하세요. ### MIX_COVER_TEMPLATE 표지는 템플릿(SweetBook 카탈로그) + 내지는 PDF(파트너 직접 제작)를 결합한 방식입니다. 1. **책 생성:** `POST /books` with `creationType: "MIX_COVER_TEMPLATE"` + `pageCount`(내지 PDF 페이지 수, 필수). 2. **(선택) 사진 사전 업로드:** 표지 템플릿에 사용할 사진을 미리 업로드 가능 (`POST /books/{uid}/photos`). 3. **표지 추가:** `POST /books/{uid}/cover`로 표지 템플릿 + 파라미터 바인딩. 4. **내지 PDF 업로드:** `POST /books/{uid}/pdf-contents` / `PUT /books/{uid}/pdf-contents` (교체). 5. **책 최종화:** `POST /books/{uid}/finalization`. 표지 렌더링이 트리거되며 `pdfStatus`가 `1`(PENDING, 렌더링 대기)로 전환됩니다. 6. **주문:** `POST /orders`. 주문 상태는 `PAID`로 시작하며, 표지 렌더링 완료 후 `PDF_READY`로 전환됩니다. 상세 호출 흐름은 [혼합 방식 시나리오](/docs/guides/scenario-mix)를 참고하세요. ## 다음 단계 방식을 선택하셨다면 시나리오 페이지에서 단계별 호출 흐름을 따라하세요. - [**PDF 업로드 시나리오**](/docs/guides/scenario-pdf) — 기본 권장 방식. 책 생성·표지·내지 업로드·최종화·주문 단계별 안내. - [**템플릿 기반 시나리오**](/docs/guides/scenario-template) — 사진·텍스트 입력으로 책 구성. - [**혼합 방식 시나리오**](/docs/guides/scenario-mix) — 표지 템플릿 + 내지 PDF. ## 관련 문서 - [Books — PDF 기반 — — 엔드포인트 명세](/docs/api/books-pdf) - [Books — 템플릿 기반 — — 엔드포인트 명세](/docs/api/books-template) - [BookSpecs API — — 책 스펙 정의 (PDF 검증 기준)](/docs/api/book-specs) - [Templates API — — 사용 가능한 템플릿 카탈로그](/docs/api/templates) --- # PDF 업로드 시나리오 파트너가 직접 제작한 PDF를 업로드해 책을 만들고 주문까지 완료하는 전체 흐름입니다. 표지·내지 PDF 업로드 후 `POST /books/{bookUid}/finalization`으로 책을 최종화하면 FINALIZED 상태가 되고, 주문 생성 시 즉시 PDF_READY 상태로 진입합니다. ## 개요 ### 호출 시퀀스 - **책 생성** — `creationType=PDF_UPLOAD`, `pageCount` 지정 - **표지 PDF 업로드** + **내지 PDF 업로드** (순서 무관) - **책 최종화** — `POST /books/{bookUid}/finalization` 호출로 책을 FINALIZED 상태로 전환 - **주문 생성** — finalize 후 주문 시 즉시 `PDF_READY` 상태 진입 - **주문 상태 추적** — 웹훅으로 배송 진행 수신 ### 사용 케이스 이미 자체 편집 도구로 PDF를 직접 제작하고 있거나, 템플릿 기반 렌더링 없이 파트너 쪽에서 최종 레이아웃을 제어하고 싶을 때 사용합니다. ## 예시 환경 이 페이지의 모든 curl 예시는 아래 값을 기준으로 작성되어 있습니다. 코드 블록 우측 상단의 **Sandbox / Live** 탭으로 환경이 전환됩니다. 복붙해서 실행할 때는 `{YOUR_API_KEY}`를 본인 API Key로, `{bookUid}` 플레이스홀더를 실제 값으로 교체하세요. | 항목 | 예시 값 | | --- | --- | | 판형 UID (`bookSpecUid`) | `SQUAREBOOK_HC` (하드커버 243×248mm) | | 페이지 수 (`pageCount`) | `24` | | 표지 PDF | `hardcover24.pdf` (544×288mm, 24p 기준) | | 내지 PDF | `hardcontents24.pdf` (249×254mm, 24p) | ## PDF 준비 요건 업로드 시 PDF 크기·페이지 수·파일 무결성이 동기 검증됩니다. 검증 실패 시 400 에러로 업로드가 차단됩니다. ### 판형별 PDF 규격 이 페이지의 예시는 **SQUAREBOOK_HC(하드커버)** 기준 고정 규격을 사용합니다 (위 예시 환경 블록 참조). 다른 판형의 PDF 규격은 **파트너 포털**에서 확인하세요. 커버 타입(하드/소프트)에 따라 규격 규칙이 다릅니다. | 커버 타입 | 표지 규격 | 내지 규격 | | --- | --- | --- | | 소프트커버 | 고정 크기 (단일 객체) | 고정 크기 | | 하드커버 | 페이지 수 범위별 (배열) — 책등 너비에 따라 가변 | 고정 크기 | ### 허용 오차 | 항목 | 허용 오차 | | --- | --- | | 소프트커버 표지 (너비·높이 공통) | ±1mm | | 하드커버 표지 너비 (width) | ±2mm | | 하드커버 표지 높이 (height) | ±1mm | | 내지 크기 (너비·높이 공통) | ±1mm | ### 제본 타입별 내지 PDF 형식 | 제본 타입 | 내지 PDF 형식 | 업로드 PDF 페이지수 | | --- | --- | --- | | PUR (예: `SQUAREBOOK_HC`, `PHOTOBOOK_A4_SC`) | 단면 (page-by-page) | `book.pageCount` 그대로 | | LAYFLAT (예: `SQUAREBOOK_LAYFLAT_HC`) | 펼침면 (spread, 좌·우 펼침) | `book.pageCount / 2` | ※ **표지 PDF는 제본 타입과 관계없이 항상 펼침면 1페이지**(앞표지 + 책등 + 뒤표지 한 장)입니다. `book.pageCount`·`bookSpec.pageMin`·`bookSpec.pageMax`·`bookSpec.pageIncrement`는 모두 **책 페이지 단위**로 저장됩니다. 업로드 검증 시점에만 PDF 페이지수로 환산됩니다. ### 공통 제약 (업로드 차단) - 파일 크기: **500MB 이하** - 파일 형식: **PDF** (시그니처 `%PDF-` 확인) - PDF 구조 무결성: 손상/파싱 실패 시 업로드 차단 - 내지 페이지 수: 책 생성 시 `pageCount`와 **정확히 일치**해야 함 (LAYFLAT은 `pageCount / 2` PDF 페이지 — 불일치 시 400) - PDF 크기: 허용 오차 내 (위 표 참조) ### 페이지 수 주의사항 (업로드는 성공, 주문 시 차단) 책 생성 시 지정한 `pageCount`는 판형의 페이지 규칙을 따라야 합니다. 규칙에 맞지 않아도 **PDF 업로드 자체는 성공**하지만 (응답의 `warnings` 배열에 경고 포함), **주문 생성 시 400 에러로 차단**됩니다. | 규칙 | 설명 | 예시 (SQUAREBOOK_HC) | | --- | --- | --- | | 페이지 수 범위 | `pageCount`가 판형의 `pageMin` ~ `pageMax` 사이여야 함 | 24 ~ 130 | | 페이지 수 배수 | `pageCount`가 `pageIncrement`의 배수여야 함 | 2의 배수 (24, 26, 28, ...) | 예를 들어 SQUAREBOOK_HC에서 `pageCount=23`으로 책을 생성하면 23p PDF 업로드는 성공하지만, `pageIncrement=2`의 배수가 아니므로 주문이 거부됩니다. 반드시 **짝수이면서 판형 범위 내**인 페이지 수를 지정하세요. **PDF 규격 구조 — 하드커버 예시 (파트너 포털에서 제공)** ```json { "cover": [ { "minPage": 24, "maxPage": 64, "width": 544, "height": 288 }, { "minPage": 65, "maxPage": 130, "width": 550, "height": 288 } ], "inner": { "width": 249, "height": 254 } } ``` **PDF 규격 구조 — 소프트커버 예시 (파트너 포털에서 제공)** ```json { "cover": { "width": 332, "height": 216 }, "inner": { "width": 148, "height": 210 } } ``` ## 1. 책 생성 POST `/books` PDF 업로드 방식으로 책을 생성합니다. `creationType="PDF_UPLOAD"`와 `pageCount`(내지 페이지 수)를 지정하는 것이 템플릿 기반 방식과의 차이점입니다. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `title` | string | O | 책 제목 (1~255자) | | `bookSpecUid` | string | O | 판형 UID | | `creationType` | string | O | `"TEMPLATE"` / `"PDF_UPLOAD"` / `"MIX_COVER_TEMPLATE"` 중 하나. PDF 업로드 방식에선 `"PDF_UPLOAD"` 지정 | | `pageCount` | integer | - | 내지 페이지 수 (단페이지 기준). `creationType=PDF_UPLOAD` 또는 `MIX_COVER_TEMPLATE`일 때 필수 | | `externalRef` | string | - | 파트너 외부 참조 식별자 (최대 100자). 파트너 시스템의 고유 ID를 저장하는 용도 | 상태 전이: `(없음)` → `DRAFT(편집중)`. 응답에서 받은 `bookUid`를 이후 모든 단계에서 사용합니다. > ℹ️ **Idempotency-Key 지원:** `POST /books` 요청 시 `Idempotency-Key` 헤더를 제공하면 동일한 키로 재요청해도 책이 중복 생성되지 않습니다. 네트워크 오류 등으로 인한 재시도 시 활용하세요. ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | `creationType=PDF_UPLOAD`인데 `pageCount` 누락 | 400 | `creation_type=PDF_UPLOAD는 pageCount(내지 페이지수)가 필수입니다.` | | 유효하지 않은 `creationType` 값 | 400 | `유효하지 않은 creationType: INVALID_TYPE (허용: TEMPLATE, PDF_UPLOAD, MIX_COVER_TEMPLATE)` | **책 생성** ```bash curl -X POST 'https://api.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: unique-request-id-001' \\ -d '{ "title": "PDF 업로드 샘플", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "PDF_UPLOAD", "pageCount": 24, "externalRef": "PARTNER-ORDER-001" }' ``` **Response (201 Created)** ```json { "success": true, "message": "책 생성 완료", "data": { "bookUid": "bk_3lOO********" } } ``` ## 2. 표지 PDF 업로드 POST / PUT `/books/{bookUid}/pdf-cover` 표지 PDF를 업로드하고 동기 검증합니다. 메서드에 따라 동작이 분리되어 있습니다. - **`POST`** — **신규 등록** 전용. 이미 등록되어 있으면 `409 Conflict` - **`PUT`** — **교체** 전용. 등록되어 있지 않으면 `404 Not Found` **교체 실패 시 기존 파일 보존** — PUT 요청의 검증이 실패해도, 이미 성공적으로 업로드되어 있던 파일은 그대로 유지됩니다. ### Request (multipart/form-data) | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `file` | file (application/pdf) | O | 표지 PDF 파일 (1페이지 고정) | ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `valid` | boolean | 검증 통과 여부 | | `pageCount` | integer | PDF의 실제 페이지 수 (표지는 1) | | `pdfSizeMm` | object | PDF 실제 크기 (`width`, `height`, mm 단위) | | `messages` | string[] | 안내 메시지 배열 | | `warnings` | string[] | 경고 메시지 배열 | | `bookStatus` | integer | 책의 현재 상태. `1`=DRAFT(편집중) / `2`=FINALIZED(최종화) | | `url` | string | 업로드된 PDF 조회 URL | ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | POST 호출 시 표지 PDF가 이미 등록됨 | 409 | `표지 PDF가 이미 등록되어 있습니다. 교체하려면 PUT 메서드를 사용하세요.` | | PUT 호출 시 표지 PDF가 등록되어 있지 않음 | 404 | `표지 PDF가 등록되어 있지 않습니다. 신규 등록은 POST 메서드를 사용하세요.` | | PDF 크기 불일치 | 400 | `PDF 너비 불일치: 실제 148.0mm, 허용 규격 249mm (+/-1mm)` | **표지 신규 등록 (POST)** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/pdf-cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcover24.pdf;type=application/pdf' ``` **Response (201 Created)** ```json { "success": true, "message": "cover PDF uploaded successfully", "data": { "bookUid": "bk_3lOO********", "kind": "cover", "valid": true, "pageCount": 1, "pdfSizeMm": { "width": 544, "height": 288 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_3lOO********/pdf-cover" } } ``` **표지 교체 (PUT)** ```bash curl -X PUT 'https://api.sweetbook.com/v1/books/{bookUid}/pdf-cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcover24.pdf;type=application/pdf' ``` **Response (200 OK)** ```json { "success": true, "message": "cover PDF replaced successfully", "data": { "bookUid": "bk_3lOO********", "kind": "cover", "valid": true, "pageCount": 1, "pdfSizeMm": { "width": 544, "height": 288 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_3lOO********/pdf-cover" } } ``` ## 3. 내지 PDF 업로드 POST / PUT `/books/{bookUid}/pdf-contents` 내지 PDF를 업로드합니다. 실제 페이지 수가 책 생성 시 지정한 `pageCount`와 **정확히 일치**해야 합니다 (불일치 시 400). - **`POST`** — **신규 등록** 전용. 이미 등록되어 있으면 `409 Conflict` - **`PUT`** — **교체** 전용. 등록되어 있지 않으면 `404 Not Found` ### Request (multipart/form-data) | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `file` | file (application/pdf) | O | 내지 PDF 파일. 페이지 수가 `pageCount`와 일치해야 함 | ### 응답 필드 표지 업로드와 동일한 구조 (`kind`만 `"contents"`). 응답 필드 상세는 **2. 표지 PDF 업로드** 섹션 참조. ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | POST 호출 시 내지 PDF가 이미 등록됨 | 409 | `내지 PDF가 이미 등록되어 있습니다. 교체하려면 PUT 메서드를 사용하세요.` | | PUT 호출 시 내지 PDF가 등록되어 있지 않음 | 404 | `내지 PDF가 등록되어 있지 않습니다. 신규 등록은 POST 메서드를 사용하세요.` | | PDF 크기 불일치 | 400 | `PDF 너비 불일치: 실제 148.0mm, 허용 규격 249mm (+/-1mm)` | | 내지 PDF 페이지 수 불일치 | 400 | `내지 PDF 페이지수 불일치: 올바른 페이지수 24p, 업로드된 페이지수 30p` (LAYFLAT은 책 페이지 환산값 부가) | **내지 신규 등록 (POST)** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcontents24.pdf;type=application/pdf' ``` **Response (201 Created)** ```json { "success": true, "message": "contents PDF uploaded successfully", "data": { "bookUid": "bk_3lOO********", "kind": "contents", "valid": true, "pageCount": 24, "pdfSizeMm": { "width": 249, "height": 254 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_3lOO********/pdf-contents" } } ``` **내지 교체 (PUT)** ```bash curl -X PUT 'https://api.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcontents24.pdf;type=application/pdf' ``` **Response (200 OK)** ```json { "success": true, "message": "contents PDF replaced successfully", "data": { "bookUid": "bk_3lOO********", "kind": "contents", "valid": true, "pageCount": 24, "pdfSizeMm": { "width": 249, "height": 254 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_3lOO********/pdf-contents" } } ``` ## 4. 책 최종화 POST `/books/{bookUid}/finalization` 표지·내지 PDF가 모두 업로드된 후 호출합니다. 호출 성공 시 책의 `status`가 `draft` → `finalized`로, `pdfStatus`가 `null` → `2`(COMPLETED)로 전환되어 주문 가능 상태가 됩니다. > ⚠️ **최종화 호출이 누락되면 주문이 차단됩니다** > > 표지·내지 PDF 업로드 후 이 엔드포인트를 호출해야 책이 FINALIZED 상태가 됩니다. 호출 누락 시 주문 생성은 `400 ERR_FINALIZE_PREREQ_UNMET`으로 차단됩니다. **Request** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/finalization' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Length: 0' ``` 상세 응답 필드·에러 분기·재호출 멱등 동작은 [Books — PDF 기반 → 책 최종화](/docs/api/books-pdf#finalization)를 참고하세요. ## 5. 주문 생성 POST `/orders` 책이 `FINALIZED(최종화)` 상태인 경우에만 주문 생성이 가능합니다. PDF 업로드 방식의 책은 PDF가 이미 준비되어 있으므로 **주문 생성 시점에 즉시 `PDF_READY`** 상태로 진입합니다. 주문 전 금액 확인이 필요하면 `POST /orders/estimate`를 먼저 호출하세요. 충전금 잔액 부족 시 **402 Insufficient Credit**이 반환됩니다. 필드 상세는 [Orders API 레퍼런스](/docs/api/orders) 참조. > ℹ️ **Idempotency-Key 지원:** `POST /orders` 요청 시 `Idempotency-Key` 헤더를 제공하면 동일한 키로 재요청해도 주문이 중복 생성되지 않습니다. 네트워크 오류 등으로 인한 재시도 시 활용하세요. ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | DRAFT(편집중) 상태 책을 주문 시도 | 400 | `Book이 FINALIZED 상태가 아닙니다: bk_588b********` | | 충전금 부족 | 402 | `잔액이 부족합니다. 필요: 3420, 잔액: 760.00` | **주문 생성** ```bash curl -X POST 'https://api.sweetbook.com/v1/orders' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: unique-request-id-002' \\ -d '{ "items": [{ "bookUid": "{bookUid}", "quantity": 1 }], "shipping": { "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06134", "address1": "서울시 강남구 테헤란로 100", "address2": "101호", "memo": "" }, "externalRef": "PARTNER-ORDER-001" }' ``` **Response (402) — 충전금 부족** ```json { "success": false, "message": "Insufficient Credit", "data": { "required": 3420, "balance": 760.00, "currency": "KRW" }, "errors": [ "잔액이 부족합니다. 필요: 3420, 잔액: 760.00" ] } ``` **Response (201 Created) — 즉시 PDF_READY** ```json { "success": true, "message": "주문이 생성되었습니다", "data": { "orderUid": "or_7Bjt********", "accountName": "김영수", "accountOrganizationName": null, "externalRef": "PARTNER-ORDER-001", "orderStatus": "PDF_READY", "orderStatusDisplay": "PDF준비완료", "isTest": true, "totalProductAmount": 100.00, "totalShippingFee": 3000, "totalPackagingFee": 0, "totalAmount": 3100.00, "paidCreditAmount": 3410, "creditBalanceAfter": 98250.00, "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06134", "address1": "서울시 강남구 테헤란로 100", "address2": "101호", "shippingMemo": "", "trackingNumber": null, "trackingCarrier": null, "cancelReason": null, "refundAmount": null, "orderedAt": "2026-04-15T06:16:49.813Z", "paidAt": "2026-04-15T06:16:49.813Z", "cancelledAt": null, "shippedAt": null, "deliveredAt": null, "createdAt": "2026-04-15T06:16:49.813Z", "items": [ { "itemUid": "oi_34CN********", "bookUid": "bk_3lOO********", "bookTitle": "PDF 업로드 샘플", "bookSpecUid": "SQUAREBOOK_HC", "bookSpecName": "고화질 스퀘어북 (하드커버)", "quantity": 1, "pageCount": 24, "unitPrice": 100.00, "itemAmount": 100.00, "itemStatus": "PDF_READY", "itemStatusDisplay": "PDF준비완료", "trackingNumber": null, "trackingCarrier": null, "shippedAt": null, "createdAt": "2026-04-15T06:16:49.813Z" } ] } } ``` ## 6. 주문 상태 추적 주문이 생성된 이후 제작·배송 진행은 웹훅 이벤트로 수신합니다. PDF 업로드 방식 주문은 `PDF_READY`에서 시작하며, 이후 상태 전이 체계는 [주문 상태 흐름](/docs/operations/order-status) 페이지 참조. - 수신 이벤트 목록: [Webhook Events 레퍼런스](/docs/api/webhook-events) - 서명 검증: [Webhooks API](/docs/api/webhooks) - 상태 전이 전체도: [주문 상태 흐름](/docs/operations/order-status) ## PDF 다운로드 (선택) GET `/books/{bookUid}/pdf-cover` GET `/books/{bookUid}/pdf-contents` 업로드한 PDF를 다시 다운로드합니다. 인증 필요. 성공 시 `Content-Type: application/pdf` 바이너리 스트림이 반환됩니다. 한쪽만 업로드된 상태에서도 해당 파일은 다운로드 가능합니다. ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | 파일이 존재하지 않음 | 404 | `PDF 파일이 존재하지 않습니다.` | | 권한 없음 | 403 | `해당 책에 대한 권한이 없습니다.` | **표지 다운로드** ```bash curl -o cover.pdf 'https://api.sweetbook.com/v1/books/{bookUid}/pdf-cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **내지 다운로드** ```bash curl -o contents.pdf 'https://api.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ## 관련 문서 - [Books — PDF 기반 — — PDF 업로드 엔드포인트 명세](/docs/api/books-pdf) - [BookSpecs API — — 판형 정보 (PDF 검증 기준 포함)](/docs/api/book-specs) - [Orders API — — 주문 생성·취소·조회](/docs/api/orders) - [주문 상태 흐름 — — PAID/PDF_READY/CONFIRMED 상태 전이](/docs/operations/order-status) - [에러 코드 & 트러블슈팅 — — PDF 업로드/주문 에러 분기](/docs/operations/errors) --- # 템플릿 기반 시나리오 템플릿을 사용해 책을 구성하고, 서버에서 PDF를 렌더링한 뒤 주문까지 완료하는 전체 흐름입니다. ## 개요 ### 호출 시퀀스 - **책 생성** — 판형(`bookSpecUid`) 지정 - **표지 추가** — 표지 템플릿 + 파라미터 바인딩 (템플릿에 따라 이미지 포함) - **내지 추가** (반복) — 내지 템플릿 + 파라미터 바인딩 (템플릿에 따라 이미지 포함) - **책 최종화** — 페이지 수 검증 후 FINALIZED 상태로 전환 - **주문 생성** — `PAID` 상태 진입, 서버에서 PDF 렌더링 후 `PDF_READY`로 승격 - **주문 상태 추적** — 웹훅으로 배송 진행 수신 ### 사용 케이스 템플릿을 활용해 포토북을 만들고 싶을 때 사용합니다. 공용 템플릿 또는 파트너 전용 템플릿을 선택하고, 사진과 텍스트만 준비하면 됩니다. 각 템플릿의 디자인 특징과 사용 예시는 [템플릿 상세 정보 ↗](https://api.sweetbook.com/partner/templates)에서 확인할 수 있습니다. ## 예시 환경 이 페이지의 모든 curl 예시는 아래 값을 기준으로 작성되어 있습니다. 코드 블록 우측 상단의 **Sandbox / Live** 탭으로 환경이 전환됩니다. 복붙해서 실행할 때는 `{YOUR_API_KEY}`를 본인 API Key로, `{bookUid}` 플레이스홀더를 실제 값으로 교체하세요. | 항목 | 예시 값 | | --- | --- | | 판형 UID (`bookSpecUid`) | `SQUAREBOOK_HC` (하드커버 243×248mm) | | 표지 템플릿 UID | `79yjMH3qRPly` (일기장A 표지) | | 내지 템플릿 UID | `46VqZhVNOfAp` (일기장A 내지a) | ## 1. 책 생성 POST `/books` 새로운 책을 생성합니다. 생성된 책은 `DRAFT(편집중)` 상태로 시작되며, 이후 표지와 내지를 추가할 수 있습니다. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `title` | string | O | 책 제목 (1~255자) | | `bookSpecUid` | string | O | 판형 UID | | `externalRef` | string | - | 파트너 외부 참조 식별자 (최대 100자). 파트너 시스템의 고유 ID를 저장하는 용도 | > ℹ️ **Idempotency-Key 지원:** `POST /books` 요청 시 `Idempotency-Key` 헤더를 제공하면 동일한 키로 재요청해도 책이 중복 생성되지 않습니다. 네트워크 오류 등으로 인한 재시도 시 활용하세요. **책 생성** ```bash curl -X POST 'https://api.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: unique-request-id-001' \\ -d '{ "title": "일기장 샘플", "bookSpecUid": "SQUAREBOOK_HC", "externalRef": "PARTNER-ORDER-001" }' ``` **Response (201 Created)** ```json { "success": true, "message": "책 생성 완료", "data": { "bookUid": "bk_5WSy********" } } ``` ## 2. 표지 추가 POST `/books/{bookUid}/cover` 템플릿과 파라미터를 사용하여 표지를 생성합니다. 템플릿에 이미지 변수(`binding: "file"`)가 있으면 이미지도 함께 전달합니다. 이미 표지가 있는 상태에서 다시 호출하면 덮어쓰기됩니다. ### Request (multipart/form-data) | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `templateUid` | string | O | 표지 템플릿 UID | | `parameters` | string | - | 템플릿 파라미터 JSON 문자열. 이미지 URL, 텍스트 값 등을 전달 | | 동적 이미지 필드 | file | - | 템플릿 변수명과 일치하는 필드명으로 이미지 파일 제공 | ### 이미지 전달 방식 (이미지 변수가 있는 템플릿인 경우) 템플릿의 `parameters.definitions`에 `binding: "file"` 타입 변수가 있으면 이미지를 전달해야 합니다. 텍스트만으로 구성된 템플릿이면 이미지 없이 호출 가능합니다. | 방식 | 설명 | | --- | --- | | 파일 직접 업로드 | multipart form 필드로 이미지 파일 첨부. `parameters`에서 해당 변수를 `"$upload"`로 지정 | | URL 전달 | `parameters` JSON에 이미지 URL을 직접 지정 (50MB / 10초 타임아웃) | **표지 추가 (파일 업로드)** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=79yjMH3qRPly' \\ -F 'coverPhoto=@front.jpg;type=image/jpeg' \\ -F 'parameters={"title":"나의 하루 기록","dateRange":"26.01 - 26.04","coverPhoto":"$upload"}' ``` **Response (201 Created)** ```json { "success": true, "message": "Cover created successfully", "data": { "result": "inserted" } } ``` ## 3. 내지 추가 (반복) POST `/books/{bookUid}/contents` 내지 페이지를 추가합니다. 판형의 최소 페이지 수(`pageMin`)를 충족할 때까지 반복 호출합니다. 템플릿에 이미지 변수가 있으면 이미지도 함께 전달합니다. 이미지 전달 방식은 표지와 동일합니다 (**2. 표지 추가** 섹션 참조). ### Query Parameters | 파라미터 | 기본값 | 설명 | | --- | --- | --- | | `breakBefore` | content→none, divider/publish→page | **page**: 새 페이지에서 시작 / **column**: 여유 컬럼에 배치 / **none**: 이전 콘텐츠 바로 다음에 연속 | ### Request (multipart/form-data) | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `templateUid` | string | O | 내지 템플릿 UID (`content`, `divider`, `publish` 중 하나) | | `parameters` | string | - | 템플릿 파라미터 JSON 문자열 | | 동적 이미지 필드 | file | - | 템플릿 변수명과 일치하는 필드명으로 이미지 파일 제공 | ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `result` | string | `inserted` (신규) 또는 `updated` (기존 교체) | | `breakBefore` | string | 적용된 breakBefore 값 | | `pageNum` | int | 배치된 페이지 번호 | | `pageSide` | string | `left` 또는 `right` | | `pageCount` | int | 현재까지 총 페이지 수 | **내지 추가** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/contents?breakBefore=page' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=46VqZhVNOfAp' \\ -F 'photo=@photo1.jpg;type=image/jpeg' \\ -F 'parameters={"monthNum":"04","dayNum":"14","diaryText":"오늘의 일기입니다.","photo":"$upload"}' ``` **Response (201 Created)** ```json { "success": true, "message": "Content created successfully", "data": { "result": "inserted", "breakBefore": "page", "pageNum": 1, "pageSide": "right", "pageCount": 0 } } ``` ## 4. 책 최종화 POST `/books/{bookUid}/finalization` 책을 FINALIZED(최종화) 상태로 전환합니다. 페이지 수가 판형의 `pageMin` 이상이어야 하며, `pageIncrement` 배수 조건도 충족해야 합니다. ### 페이지 수 규칙 (SQUAREBOOK_HC 기준) | 규칙 | 값 | | --- | --- | | 최소 페이지 수 (`pageMin`) | 24 | | 최대 페이지 수 (`pageMax`) | 130 | | 페이지 증분 (`pageIncrement`) | 2 | ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `result` | string | 최종화 결과 메시지 | | `pageCount` | int | 최종 페이지 수 | | `finalizedAt` | string | 최종화 완료 시각 (ISO 8601) | ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | 최소 페이지 미달 | 400 | `최소 페이지 미달: 현재 0p, 최소 24p` | **최종화** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/finalization' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Response (201 Created)** ```json { "success": true, "message": "책 최종화 완료", "data": { "result": "페이지를 추가하고 완료", "pageCount": 26, "finalizedAt": "2026-04-14T07:15:44.613Z" } } ``` ## 5. 주문 생성 POST `/orders` 책이 `FINALIZED(최종화)` 상태인 경우에만 주문 생성이 가능합니다. 주문 생성 시 `PAID` 상태로 진입하며, 서버에서 PDF 렌더링이 완료되면 `PDF_READY`로 승격됩니다. 주문 전 금액 확인이 필요하면 `POST /orders/estimate`를 먼저 호출하세요. 충전금 잔액 부족 시 **402 Insufficient Credit**이 반환됩니다. 필드 상세는 [Orders API 레퍼런스](/docs/api/orders) 참조. > ℹ️ **Idempotency-Key 지원:** `POST /orders` 요청 시 `Idempotency-Key` 헤더를 제공하면 동일한 키로 재요청해도 주문이 중복 생성되지 않습니다. 네트워크 오류 등으로 인한 재시도 시 활용하세요. ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | DRAFT(편집중) 상태 책을 주문 시도 | 400 | `Book이 FINALIZED 상태가 아닙니다: bk_588b********` | | 충전금 부족 | 402 | `잔액이 부족합니다. 필요: 3420, 잔액: 760.00` | **주문 생성** ```bash curl -X POST 'https://api.sweetbook.com/v1/orders' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: unique-request-id-002' \\ -d '{ "items": [{ "bookUid": "{bookUid}", "quantity": 1 }], "shipping": { "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06134", "address1": "서울시 강남구 테헤란로 100", "address2": "101호", "memo": "" }, "externalRef": "PARTNER-ORDER-001" }' ``` **Response (402) — 충전금 부족** ```json { "success": false, "message": "Insufficient Credit", "data": { "required": 3420, "balance": 760.00, "currency": "KRW" }, "errors": [ "잔액이 부족합니다. 필요: 3420, 잔액: 760.00" ] } ``` **Response (201 Created) — PAID** ```json { "success": true, "message": "주문이 생성되었습니다", "data": { "orderUid": "or_41qi********", "accountName": "김영수", "accountOrganizationName": null, "externalRef": "PARTNER-ORDER-001", "orderStatus": "PAID", "orderStatusDisplay": "결제완료", "isTest": true, "totalProductAmount": 110.00, "totalShippingFee": 3000, "totalPackagingFee": 0, "totalAmount": 3110.00, "paidCreditAmount": 3420, "creditBalanceAfter": 93160.00, "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06134", "address1": "서울시 강남구 테헤란로 100", "address2": "101호", "shippingMemo": "", "trackingNumber": null, "trackingCarrier": null, "cancelReason": null, "refundAmount": null, "orderedAt": "2026-04-14T07:15:47.527Z", "paidAt": "2026-04-14T07:15:47.527Z", "cancelledAt": null, "shippedAt": null, "deliveredAt": null, "createdAt": "2026-04-14T07:15:47.527Z", "items": [ { "itemUid": "oi_4nfx********", "bookUid": "bk_5WSy********", "bookTitle": "일기장 샘플", "bookSpecUid": "SQUAREBOOK_HC", "bookSpecName": "고화질 스퀘어북 (하드커버)", "quantity": 1, "pageCount": 26, "unitPrice": 110.00, "itemAmount": 110.00, "itemStatus": "PAID", "itemStatusDisplay": "결제완료", "trackingNumber": null, "trackingCarrier": null, "shippedAt": null, "createdAt": "2026-04-14T07:15:47.527Z" } ] } } ``` ## 6. 주문 상태 추적 주문이 생성된 이후 제작·배송 진행은 웹훅 이벤트로 수신합니다. 템플릿 기반 방식 주문은 `PAID`에서 시작하며, 서버에서 PDF 렌더링이 완료되면 `PDF_READY`로 승격됩니다. 이후 상태 전이 체계는 [주문 상태 흐름](/docs/operations/order-status) 페이지 참조. - 수신 이벤트 목록: [Webhook Events 레퍼런스](/docs/api/webhook-events) - 서명 검증: [Webhooks API](/docs/api/webhooks) - 상태 전이 전체도: [주문 상태 흐름](/docs/operations/order-status) ## 관련 문서 - [Books — 템플릿 기반 — — 표지·내지 추가·최종화 엔드포인트 명세](/docs/api/books-template) - [Templates API — — 표지·내지 템플릿 카탈로그·변수 정보](/docs/api/templates) - [BookSpecs API — — 판형 정보 (페이지 수 규칙 포함)](/docs/api/book-specs) - [템플릿 구조와 바인딩 — — 변수·파라미터·이미지 매칭 동작 원리](/docs/concepts/template-structure) - [Orders API — — 주문 생성·취소·조회](/docs/api/orders) - [주문 상태 흐름 — — PAID/PDF_READY 이후 상태 전이](/docs/operations/order-status) --- # 혼합 방식 시나리오 (템플릿 표지 + PDF 내지) 표지는 템플릿으로 렌더링하고 내지는 파트너가 직접 제작한 PDF로 업로드하는 혼합 방식입니다. 표지 디자인을 SweetBook 템플릿 시스템에 맡기면서, 내지 편집은 파트너 시스템에서 제어하고 싶을 때 사용합니다. ## 개요 ### 호출 시퀀스 - **책 생성** — `creationType=MIX_COVER_TEMPLATE`, `pageCount`(내지 페이지 수) 지정 - **표지 추가** — `POST /books/{bookUid}/cover`로 **템플릿 기반** 표지 적용 - **내지 PDF 업로드** — `POST /books/{bookUid}/pdf-contents`로 내지 PDF 업로드 - **책 최종화** — `POST /books/{bookUid}/finalization` 호출로 책을 FINALIZED 상태로 전환 - **주문 생성** — `POST /orders` (TEMPLATE과 동일 흐름) - **주문 상태 추적** — 웹훅으로 렌더링·배송 진행 수신 ### 사용 케이스 표지는 SweetBook이 제공하는 **테마별 표지 템플릿**의 디자인을 활용하면서, 내지는 파트너 자체 편집 도구에서 만든 PDF를 그대로 인쇄하고 싶을 때 사용합니다. 표지 디자인 품질과 내지 편집 자율성을 동시에 얻을 수 있습니다. > ℹ️ **MIX 핵심 규약** > > 표지는 **템플릿 전용** (`/cover`), 내지는 **PDF 전용** (`/pdf-contents`)입니다. > 반대로 시도하면 400 에러가 반환됩니다 (상세는 아래 **제약사항** 섹션 참조). ## 예시 환경 이 페이지의 모든 curl 예시는 아래 값을 기준으로 작성되어 있습니다. 복붙해서 실행할 때는 `{YOUR_API_KEY}`를 본인 API Key로, `{bookUid}` 플레이스홀더를 실제 값으로 교체하세요. | 항목 | 예시 값 | | --- | --- | | 판형 UID (`bookSpecUid`) | `SQUAREBOOK_HC` (하드커버 243×248mm) | | 페이지 수 (`pageCount`, 내지) | `24` | | 표지 템플릿 (`templateUid`) | `79yjMH3qRPly` (일기장A 표지) | | 내지 PDF | `hardcontents24.pdf` (249×254mm, 24p) | ## 1. 책 생성 POST `/books` 혼합 방식 책을 생성합니다. `creationType="MIX_COVER_TEMPLATE"`와 `pageCount`(내지 페이지 수)를 함께 지정합니다. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `title` | string | O | 책 제목 (1~255자) | | `bookSpecUid` | string | O | 판형 UID | | `creationType` | string | O | `"MIX_COVER_TEMPLATE"` 지정 | | `pageCount` | integer | O | 내지 페이지 수 (단페이지 기준, 양수) | | `externalRef` | string | - | 파트너 외부 참조 식별자 (최대 100자) | ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | `pageCount` 누락 | 400 | `creation_type=MIX_COVER_TEMPLATE는 pageCount(내지 페이지수)가 필수입니다.` | | `creationType` 누락 | 400 | `creationType은 필수입니다` | **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "title": "mix_verify_20260422", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "MIX_COVER_TEMPLATE", "pageCount": 24 }' ``` **Response (201 Created)** ```json { "success": true, "message": "책 생성 완료", "data": { "bookUid": "bk_2IyK********" } } ``` ## 2. 표지 템플릿 적용 POST `/books/{bookUid}/cover` **템플릿 기반** 표지를 적용합니다. 템플릿 UID와 템플릿이 요구하는 `parameters`(텍스트·이미지 바인딩)를 함께 전송합니다. MIX 방식에서는 **PDF 표지(`/pdf-cover`)가 차단**됩니다. 아래 에러 표와 **제약사항** 섹션 참조. 템플릿 파라미터 규약·이미지 전달 방법 등 상세는 [템플릿 기반 시나리오](/docs/guides/scenario-template) 및 [템플릿 구조](/docs/concepts/template-structure) 문서를 참조하세요. **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=79yjMH3qRPly' \\ -F 'parameters={"title":"MIX Test","coverPhoto":"https://example.com/cover.jpg","dateRange":"26.01 - 26.04"}' ``` **Response (201 Created)** ```json { "success": true, "message": "Cover created successfully", "data": { "result": "inserted" } } ``` ## 3. 내지 PDF 업로드 POST / PUT `/books/{bookUid}/pdf-contents` 내지 PDF를 업로드합니다. 실제 페이지 수가 책 생성 시 지정한 `pageCount`와 **정확히 일치**해야 합니다 (불일치 시 400). - **`POST`** — **신규 등록** 전용. 이미 등록되어 있으면 `409 Conflict` - **`PUT`** — **교체** 전용. 등록되어 있지 않으면 `404 Not Found` Request/Response 필드 상세 및 에러 규약은 [PDF 업로드 시나리오](/docs/guides/scenario-pdf)의 **4. 내지 PDF 업로드** 섹션과 동일합니다 (POST/PUT 분리, 409/404 에러, pdfSizeMm 검증 등). **내지 신규 등록 (POST)** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcontents24.pdf;type=application/pdf' ``` **Response (201 Created)** ```json { "success": true, "message": "contents PDF uploaded successfully", "data": { "bookUid": "bk_2IyK********", "kind": "contents", "valid": true, "pageCount": 24, "pdfSizeMm": { "width": 249, "height": 254 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_2IyK********/pdf-contents" } } ``` ## 4. 책 최종화 POST `/books/{bookUid}/finalization` 책을 최종화하여 주문 가능한 상태로 전환합니다. 내지 PDF가 업로드되어 있어야 하며, 표지 템플릿은 finalize 후 백그라운드에서 PDF로 렌더링됩니다. `pageCount`는 책 생성 시 지정한 값(내지 페이지 수)이 그대로 유지됩니다. ### 에러 응답 | 상황 | 상태 코드 | 응답 메시지 | | --- | --- | --- | | 내지 PDF 미업로드 상태에서 호출 | 400 | `내지 PDF가 업로드되어 있어야 finalize 가능합니다.` | **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/finalization' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Length: 0' ``` **Response (201 Created)** ```json { "success": true, "message": "책 최종화 완료", "data": { "result": "페이지를 추가하고 완료", "pageCount": 24, "finalizedAt": "2026-04-22T01:03:11.323Z" } } ``` ## 5. 주문 생성 POST `/orders` 주문을 생성합니다. 혼합 방식은 **TEMPLATE 방식과 동일한 주문 흐름**을 따릅니다 — 주문 즉시 `PAID` 상태로 진입하며, 렌더링이 완료되면 `PDF_READY`로 전이합니다. (PDF 업로드 방식과 달리 `PDF_READY` 즉시 전환은 일어나지 않습니다.) 주문 요청 바디·배송 필드 상세는 [Orders API](/docs/api/orders) 문서를 참조하세요. **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/orders' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: mix-order-001' \\ -d '{ "items": [ { "bookUid": "bk_2IyK********", "quantity": 1 } ], "shipping": { "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06234", "address1": "서울시 강남구 테헤란로 1", "address2": "101호" } }' ``` **Response (201 Created)** ```json { "success": true, "message": "주문이 생성되었습니다", "data": { "orderUid": "or_184U********", "orderStatus": "PAID", "orderStatusDisplay": "결제완료", "totalAmount": 3100.00, "paidCreditAmount": 3410, "items": [ { "itemUid": "oi_34Hu********", "bookUid": "bk_2IyK********", "bookSpecUid": "SQUAREBOOK_HC", "quantity": 1, "pageCount": 24, "itemStatus": "PAID", "itemStatusDisplay": "결제완료" } ] } } ``` ## 6. 주문 상태 추적 주문 생성 이후의 상태 전이와 웹훅 이벤트는 TEMPLATE 방식과 동일합니다. 상태 코드·흐름은 [주문 상태 흐름](/docs/operations/order-status), 웹훅 이벤트 목록은 [Webhook Events](/docs/api/webhook-events)를 참조하세요. ## 제약사항 / 흔한 실수 혼합 방식은 표지와 내지에 서로 다른 API를 사용하므로, 반대 방향으로 호출하면 400 에러가 반환됩니다. | 시도 | 응답 메시지 | | --- | --- | | `POST /books/{bookUid}/pdf-cover` (PDF 표지) | `MIX_COVER_TEMPLATE 타입 책의 표지는 템플릿으로 생성됩니다. /books/{bookUid}/cover API를 사용하세요.` | | `POST /books/{bookUid}/contents` (템플릿 내지) | `MIX_COVER_TEMPLATE 타입 책은 내지 템플릿 API를 사용할 수 없습니다. /books/{bookUid}/pdf-contents API를 사용하세요.` | | 내지 PDF 없이 `POST /finalization` | `내지 PDF가 업로드되어 있어야 finalize 가능합니다.` | ### 다른 방식과의 선택 기준 - 표지·내지 모두 템플릿으로 처리 → [TEMPLATE](/docs/guides/scenario-template) - 표지·내지 모두 PDF로 업로드 → [PDF_UPLOAD](/docs/guides/scenario-pdf) - 표지 템플릿 + 내지 PDF (이 페이지) → `MIX_COVER_TEMPLATE` ## 관련 문서 - [Books — PDF 기반 — — 내지 PDF 업로드 엔드포인트 명세](/docs/api/books-pdf) - [Books — 템플릿 기반 — — 표지 템플릿 추가 엔드포인트 명세](/docs/api/books-template) - [Templates API — — 표지 템플릿 카탈로그·변수 정보](/docs/api/templates) - [BookSpecs API — — 판형 정보 (PDF 검증 기준 포함)](/docs/api/book-specs) - [Orders API — — 주문 생성·취소·조회](/docs/api/orders) - [주문 상태 흐름 — — PAID/PDF_READY 이후 상태 전이](/docs/operations/order-status) --- # Books API — PDF 기반 파트너가 제작한 PDF 파일(표지·내지)을 직접 업로드하여 책을 생성하는 방식의 API 레퍼런스입니다. 템플릿 바인딩이 아닌 PDF 직접 업로드 흐름과 각 엔드포인트의 정확한 스펙을 다룹니다. > ℹ️ **템플릿 방식**(`creationType="TEMPLATE"`)은 [Books — 템플릿 기반](/docs/api/books-template) 페이지를 참고하세요. > 실제 호출 흐름을 단계별 따라 하기로 보려면 [PDF 업로드 시나리오](/docs/guides/scenario-pdf)를 참고하세요. > ℹ️ 모든 Books API는 인증이 필요합니다. API Key를 `Authorization: Bearer {YOUR_API_KEY}` 헤더로 제공하세요. 자세한 인증 안내는 [인증 가이드](/docs/authentication)를 참고하세요. ## 이 방식이 적합한 경우 - 이미 자체 제작한 PDF 파일이 있는 경우 - 인쇄용 PDF를 직접 디자인 도구(InDesign, Illustrator 등)로 제작하는 경우 - 레이아웃·폰트·이미지를 파트너가 완전히 제어하고 싶은 경우 ### 지원 `creationType` | `creationType` | 표지 | 내지 | 비고 | | --- | --- | --- | --- | | `PDF_UPLOAD` | PDF 직접 업로드 | PDF 직접 업로드 | 이 페이지의 기본 흐름 | | `MIX_COVER_TEMPLATE` | 템플릿 바인딩 | PDF 직접 업로드 | 표지 추가는 [Books — 템플릿 기반](/docs/api/books-template)의 `POST /books/{bookUid}/cover` 사용 | `TEMPLATE` 방식은 표지·내지 모두 템플릿 바인딩으로 진행하며, 이 페이지의 PDF 업로드 엔드포인트(`/pdf-cover`, `/pdf-contents`)는 사용할 수 없습니다. [Books — 템플릿 기반](/docs/api/books-template)을 참고하세요. ## 책 생성 POST `/books` 새로운 책을 생성합니다. 생성 직후 `status="draft"` 상태이며, 이후 PDF 업로드와 최종화 단계를 거쳐 주문 가능 상태로 진입합니다. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `title` | string | O | 책 제목 (1~255자) | | `bookSpecUid` | string | O | 판형 UID (예: `SQUAREBOOK_HC`). [BookSpecs API](/docs/api/book-specs)로 조회 | | `creationType` | string | O | `"PDF_UPLOAD"` 또는 `"MIX_COVER_TEMPLATE"`. 그 외 값은 400 에러 | | `pageCount` | integer | O | 내지 페이지 수. **PDF 방식은 필수**. 업로드할 내지 PDF의 실제 페이지 수와 정확히 일치해야 함 (불일치 시 내지 업로드 단계에서 400) | | `specProfileUid` | string | — | SpecProfile UID (선택, 일반적으로 미사용). 사용자별 판형 프로파일이 사전 등록된 경우에만 사용. 제공 시 해당 프로필과 연결, 유효하지 않으면 400 | | `externalRef` | string | — | 파트너 외부 참조 식별자 (최대 100자). 파트너 시스템의 고유 ID 매핑용 | > ℹ️ **이 단계에서는 빈 책만 생성**됩니다. 응답으로 받은 `bookUid`를 사용해 표지·내지 PDF 업로드와 최종화를 순차적으로 진행해야 주문 가능한 상태(`finalized`)가 됩니다. > ℹ️ **Idempotency-Key 지원**: `POST /books`는 `Idempotency-Key` 헤더를 지원합니다. 동일한 키로 재요청해도 책이 중복 생성되지 않으므로, 네트워크 오류 등 재시도 상황에 활용하세요. > 재시도가 캐시된 응답으로 처리된 경우 응답 헤더에 `X-Idempotent-Replayed: true`가 포함됩니다. > 자세한 동작은 [멱등성 가이드](/docs/operations/idempotency)를 참고하세요. ### Request 예시 **Request body (PDF_UPLOAD)** ```json { "title": "PDF 업로드 샘플", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "PDF_UPLOAD", "pageCount": 24, "externalRef": "PARTNER-ORDER-001" } ``` **Request body (MIX_COVER_TEMPLATE)** ```json { "title": "혼합 방식 샘플", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "MIX_COVER_TEMPLATE", "pageCount": 24, "externalRef": "PARTNER-ORDER-002" } ``` **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Idempotency-Key: unique-request-id-001' \\ -H 'Content-Type: application/json' \\ -d '{ "title": "PDF 업로드 샘플", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "PDF_UPLOAD", "pageCount": 24, "externalRef": "PARTNER-ORDER-001" }' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 생성된 책의 UID | | `pageMeta` | object | 페이지 제약 메타. `currentPageCount`·`pageMin`·`pageMax`·`pageIncrement`·`isValid` 5개 필드 | ### Response 예시 **Response (201 Created)** ```json { "success": true, "message": "책 생성 완료", "data": { "bookUid": "bk_2h4L********", "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true } } } ``` **pageMeta:** PDF_UPLOAD·MIX_COVER_TEMPLATE 방식은 책 생성 시 `pageCount`를 입력받으므로 `currentPageCount`가 입력값과 동일하게 설정됩니다(예: `pageCount: 24` → `currentPageCount: 24`). TEMPLATE 방식은 `0`으로 초기화됩니다. `isValid`는 `pageMin ≤ currentPageCount ≤ pageMax` 및 증분 규칙 만족 여부를 나타내며, 페이지 제약만 판정하고 표지·내지 PDF 업로드 여부는 별도입니다. ### HTTP 상태 코드 | 코드 | `errorCode` | 설명 | | --- | --- | --- | | 201 Created | — | 책 생성 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `title`/`bookSpecUid`/`creationType` 누락 또는 허용되지 않은 값, PDF 방식인데 `pageCount` 누락, 잘못된 `specProfileUid` 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 422 Unprocessable Entity | `ERR_IDEMPOTENCY_KEY_MISMATCH` | 같은 `Idempotency-Key`로 다른 본문 전송 | | 429 Too Many Requests | `ERR_TOO_MANY_REQUESTS` | 요청 빈도 제한 초과 — [Rate Limiting 가이드](/docs/operations/rate-limiting) 참고 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 본 엔드포인트의 주요 검증 실패 메시지(`400 ERR_VALIDATION_FAILED`): - `creationType` 누락 또는 허용되지 않은 값 — `errors[0]`: `"유효하지 않은 creationType: <값> (허용: TEMPLATE, PDF_UPLOAD, MIX_COVER_TEMPLATE)"` - PDF 방식(`PDF_UPLOAD` · `MIX_COVER_TEMPLATE`)인데 `pageCount` 누락 — `errors[0]`: `"creation_type=<값>는 pageCount(내지 페이지수)가 필수입니다."` 응답 6필드 shape, 다른 errorCode, 처리 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ## PDF 파일 요건 표지·내지 PDF 업로드 시 PDF 형식·크기·페이지 수가 동기 검증됩니다. 업로드 단계에서 차단되는 항목(hard fail)과 업로드는 성공하지만 주문 단계에서 차단되는 항목(soft warning)이 구분됩니다. 업로드 전에 아래 요건을 충족하는지 확인하세요. ### 판형별 PDF 규격 정확한 PDF 규격(너비·높이 mm)은 판형(BookSpec)마다 다릅니다. 구조는 커버 타입(소프트/하드)에 따라 다음과 같이 분기되며, 실제 값은 [`GET /book-specs`](/docs/api/book-specs) 또는 파트너 포털에서 확인하세요. | 커버 타입 | 표지 규격 구조 | 내지 규격 구조 | | --- | --- | --- | | **소프트커버** | 고정 크기 단일 객체 (책등 두께 무관) | 고정 크기 | | **하드커버** | 페이지 수 범위별 배열 — 책등 두께에 따라 너비 가변 | 고정 크기 | ### 허용 오차 허용 오차는 판형의 `pdfSize` 정의값을 기준으로 적용됩니다. 대부분의 판형은 **정의값과 정확히 일치(오차 0mm)**하도록 설정되어 있습니다. 안전한 인쇄 품질을 위해 PDF 제작 시 다음 권고 마진 내로 제작하는 것을 권장합니다. | 항목 | 권고 마진 (제작 시) | | --- | --- | | 소프트커버 표지 (너비·높이) | ±1mm 이내 | | 하드커버 표지 너비 | ±2mm 이내 (책등 두께 보정 여유) | | 하드커버 표지 높이 | ±1mm 이내 | | 내지 (너비·높이) | ±1mm 이내 | 실제 허용치는 판형마다 다를 수 있습니다. 정의값 정확 일치를 목표로 제작하되, 위 권고 마진 내라면 일반적으로 안전합니다. 검증 실패 시 응답의 `errors` 배열에 실제 측정값과 허용 규격이 함께 표기됩니다. ### 공통 제약 (검증 실패 시 업로드 차단 — Hard Fail) - **파일 형식**: PDF (시그니처 `%PDF-` 확인) - **파일 크기**: 500MB 이하 (요청 크기 제한과 동일) - **PDF 구조 무결성**: 손상되거나 파싱 불가능한 PDF는 차단 - **표지 페이지 수**: 1페이지 고정 - **내지 페이지 수**: 책 생성 시 지정한 `pageCount`와 정확히 일치 - **PDF 크기(mm)**: 위 표의 권고 마진 내 ### 페이지 수 권장 사항 (업로드는 성공, 주문 시 차단 — Soft Warning) 책 생성 시 지정한 `pageCount`가 판형의 페이지 규칙을 벗어나도 PDF 업로드 자체는 성공합니다. 응답의 `warnings` 배열에 경고가 포함되며, **주문 생성 시 400 에러로 차단**됩니다. | 규칙 | 설명 | 참고 (SQUAREBOOK_HC) | | --- | --- | --- | | 페이지 수 범위 | `pageCount`가 판형의 `pageMin` ~ `pageMax` 사이 | 24 ~ 130 | | 페이지 수 배수 | `pageCount`가 `pageIncrement`의 배수 | 2의 배수 (24, 26, 28, …) | 예를 들어 SQUAREBOOK_HC에서 `pageCount=23`으로 책을 생성하면 23p PDF 업로드는 성공하지만, `pageIncrement=2`의 배수가 아니므로 주문 단계에서 거부됩니다. 책 생성 시점부터 **판형 범위 내·배수 조건을 충족**하도록 지정하세요. **PDF 규격 예시 — 하드커버 (BookSpec.pdfSize)** ```json { "cover": [ { "minPage": 24, "maxPage": 64, "width": 544, "height": 288 }, { "minPage": 65, "maxPage": 130, "width": 550, "height": 288 } ], "inner": { "width": 249, "height": 254 } } ``` **PDF 규격 예시 — 소프트커버 (BookSpec.pdfSize)** ```json { "cover": { "width": 332, "height": 216 }, "inner": { "width": 148, "height": 210 } } ``` **400 ERR_VALIDATION_FAILED**: PDF 규격 검증 실패 시 `errors` 배열에 위반 항목이 모두 포함됩니다 (예: `"내지 PDF 페이지수 불일치: 올바른 페이지수 24p, 업로드된 페이지수 30p"`, `"PDF 너비 불일치: 실제 148.0mm, 허용 규격 249mm (+/-1mm)"`, `"PDF 높이 불일치: 실제 210.0mm, 허용 규격 254.0mm (+/-1mm)"`). LAYFLAT 제본은 페이지수 메시지에 책 페이지 환산값이 괄호로 부가됩니다(예: `"... (책 페이지 환산: 올바른 페이지수 16p, 업로드된 페이지수 20p)"`). 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_VALIDATION_FAILED](/docs/operations/errors#ERR_VALIDATION_FAILED)를 참조하세요. ## 표지 PDF 표지 PDF는 1페이지 고정입니다. **신규 등록(POST)**, **교체(PUT)**, **다운로드(GET)** 세 가지 엔드포인트를 제공합니다. 업로드/교체 동작은 동일한 검증을 거치며 응답 본문 구조도 같습니다 (HTTP 상태 코드와 메시지만 다름). > ⚠️ **`creationType="MIX_COVER_TEMPLATE"` 책의 표지는 PDF가 아닌 템플릿으로 생성**됩니다. > 해당 책에 PDF 표지 업로드 시도 시 400 에러가 반환됩니다. 표지는 [Books — 템플릿 기반](/docs/api/books-template)의 `POST /books/{bookUid}/cover`를 사용하세요. ### 공통 응답 필드 (POST · PUT) | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 대상 책 UID | | `kind` | string | `"cover"` 고정 | | `valid` | boolean | 검증 성공 여부 (성공 시 `true`) | | `pageMeta` | object | 책의 페이지 제약 메타. `currentPageCount`·`pageMin`·`pageMax`·`pageIncrement`·`isValid` 5필드 (v1.2 C04에서 `pageCount` 정수 필드 대체). 표지 PDF는 1페이지 고정이므로 본 응답에서는 **책**의 페이지 제약을 반환합니다. | | `pdfSizeMm` | object | `{ width, height }` mm 단위 측정값 | | `messages` | array | 검증 실패 시 에러 메시지 (성공 시 빈 배열) | | `warnings` | array | 경고 메시지 (표지는 일반적으로 비어있음, 내지에 주로 사용) | | `bookStatus` | integer | null | 현재 책 상태 (정수). `1`(DRAFT, 작성 중) / `2`(FINALIZED, 최종화 완료). 검증 실패 시 `null`이 반환될 수 있습니다. 책 목록 조회(`GET /books`) 응답의 `status` 필드는 같은 값을 문자열(`draft`/`finalized`)로 반환합니다. 표지·내지 PDF 업로드 후 `POST /books/{bookUid}/finalization`을 호출하면 `1` → `2`로 전환됩니다 | | `url` | string | null | 업로드된 PDF 다운로드 URL (검증 성공 시만 채움, 실패 시 `null`). 호출 도메인(`api-sandbox` / `api`)을 기준으로 자동 결정됨 | ### HTTP 상태 코드 (POST · PUT 공통) | 코드 | POST 신규 | PUT 교체 | | --- | --- | --- | | 200 OK | — | 교체 성공 | | 201 Created | 등록 성공 | — | | 400 Bad Request | PDF 검증 실패, 잘못된 `creationType`, 제작 확정된 주문 존재(PUT) | | | 401 Unauthorized | 인증 실패 (`ERR_UNAUTHORIZED`) | | | 403 Forbidden | 본인 소유가 아닌 책 (`ERR_FORBIDDEN`) / Sandbox 도메인에서 Live 책 또는 그 반대 (`ERR_ENV_MISMATCH`) — 아래 케이스 박스 참고 | | | 404 Not Found | — | 교체 대상 표지 PDF가 등록되어 있지 않음 | | 409 Conflict | 이미 표지 PDF가 등록되어 있음 | — | | 429 Too Many Requests | 업로드 Rate Limit 초과 — [Rate Limiting 가이드](/docs/operations/rate-limiting) 참고 | | | 500 Internal Server Error | 서버 오류 | | > ℹ️ **환경 불일치(400)**: Sandbox에서 만든 책을 Live 도메인으로 호출(또는 반대)할 때 발생합니다. 책의 `isTest` 값(`true`=Sandbox / `false`=Live)과 호출 도메인이 일치해야 합니다. > > > `{ "success": false, "message": "Invalid Operation", "errors": ["환경 불일치: 이 책은 sandbox 환경에서 생성되었습니다. sandbox 도메인에서 호출하세요."] }` ### 표지 PDF 신규 등록 POST `/books/{bookUid}/pdf-cover` 표지 PDF를 **최초로** 등록합니다. 이미 등록된 상태에서 호출하면 409 Conflict가 반환되며, 이 경우 PUT으로 교체해야 합니다. 검증은 동기로 수행되어 응답 본문에 결과가 포함됩니다. #### Request — multipart/form-data | 필드 | 위치 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | --- | | `bookUid` | path | string | O | 대상 책 UID | | `file` | form-data | file (application/pdf) | O | 표지 PDF 파일 (1페이지 고정) | 업로드 Rate Limit은 `upload` 정책(API Key 단위 200 req/분)이 적용됩니다. [Rate Limiting 가이드](/docs/operations/rate-limiting) 참고. #### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcover24.pdf;type=application/pdf' ``` #### Response 예시 **Response (201 Created)** ```json { "success": true, "message": "cover PDF가 저장되었습니다", "data": { "bookUid": "bk_2h4L********", "kind": "cover", "valid": true, "pdfSizeMm": { "width": 544, "height": 288 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_2h4L********/pdf-cover", "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true } } } ``` **409 ERR_CONFLICT**: 이미 등록된 표지에 POST 재호출 시 — `errors[0]`: `"표지 PDF가 이미 등록되어 있습니다. 교체하려면 PUT 메서드를 사용하세요."` 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_CONFLICT](/docs/operations/errors#ERR_CONFLICT)를 참조하세요. ### 표지 PDF 교체 PUT `/books/{bookUid}/pdf-cover` 이미 등록된 표지 PDF를 교체합니다. 등록되지 않은 상태에서 호출하면 404가 반환되며, 이 경우 POST로 신규 등록해야 합니다. > ⚠️ **제작 확정 이후 교체 차단**: 책에 연결된 주문 아이템 중 **제작 확정(CONFIRMED) 이상 상태**가 하나라도 있으면 PDF 교체가 차단됩니다(400). 인쇄 단계에 진입한 후의 PDF 변경을 방지하기 위함입니다. 자세한 주문 상태는 [주문 상태 흐름](/docs/operations/order-status)을 참고하세요. #### Request — multipart/form-data 신규 등록(POST)과 동일한 파라미터를 사용합니다. #### Request 예시 **Request** ```bash curl -X PUT 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcover24-v2.pdf;type=application/pdf' ``` #### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "cover PDF가 교체되었습니다", "data": { "bookUid": "bk_krW7********", "kind": "cover", "valid": true, "pdfSizeMm": { "width": 544, "height": 288 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_krW7********/pdf-cover", "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true } } } ``` **주요 에러**: `404 ERR_NOT_FOUND` 등록되지 않은 상태에서 PUT 호출 (`errors[0]`: `"표지 PDF가 등록되어 있지 않습니다. 신규 등록은 POST 메서드를 사용하세요."`) · `400 ERR_VALIDATION_FAILED` 제작 확정 이후 교체 시도 (`errors[0]`: `"제작 확정된 주문이 있어 PDF 교체가 불가합니다."`). 응답 shape 상세는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ### 표지 PDF 다운로드 GET `/books/{bookUid}/pdf-cover` 저장된 표지 PDF를 다운로드합니다. 정상 응답은 JSON이 아닌 `application/pdf` 바이너리 스트림입니다. > ℹ️ 표지 PDF의 응답은 책의 `creationType`에 따라 분기됩니다. > > - **PDF_UPLOAD** (그룹 A — 사용자 업로드): 업로드된 파일 존재 여부만 검사. 파일이 있으면 200, 없으면 404. > - **MIX_COVER_TEMPLATE** (그룹 B — 백그라운드 생성): 책 최종화 후 백엔드가 표지를 PDF로 렌더링. `pdfStatus`에 따라 200 / 409 / 422 / 500 분기. #### 응답 매트릭스 ##### 그룹 A — PDF_UPLOAD 책의 표지 | 파일 상태 | HTTP | 응답 형식 | errorCode | | --- | --- | --- | --- | | 디스크에 존재 | 200 | `application/pdf` 바이너리 스트림 | — | | 디스크에 부재 (업로드 미완) | 404 | JSON | `ERR_PDF_NOT_UPLOADED` | ##### 그룹 B — MIX_COVER_TEMPLATE 책의 표지 | 책 상태 | HTTP | 응답 형식 | errorCode | | --- | --- | --- | --- | | 책 최종화 전 (PDF 생성 미시작) | 409 | JSON | `ERR_PDF_NOT_GENERATED` | | PDF 생성 진행 중 | 409 | JSON | `ERR_PDF_PENDING` | | PDF 생성 실패 | 422 | JSON | `ERR_PDF_GENERATION_FAILED` | | 생성 완료 + 파일 부재 (서버 결함) | 500 | JSON | `ERR_PDF_FILE_MISSING` | | 생성 완료 + 파일 존재 | 200 | `application/pdf` 바이너리 스트림 | — | #### 공통 사전 검증 | 케이스 | HTTP | errorCode | | --- | --- | --- | | 책 미존재 | 404 | `ERR_NOT_FOUND` | | 본인 소유가 아닌 책 | 403 | `ERR_FORBIDDEN` | | 환경 불일치 (Sandbox/Live 도메인 ↔ 책의 환경 불일치) | 403 | `ERR_ENV_MISMATCH` | | kind 경로 오류 (`/pdf-cover`·`/pdf-contents` 외 호출) | 400 | `ERR_MALFORMED_REQUEST` | | 책의 `creationType`이 PDF 조회를 지원하지 않음 | 400 | `ERR_CREATION_TYPE_UNSUPPORTED` | #### 클라이언트 권장 행동 - `ERR_PDF_NOT_UPLOADED` (그룹 A): 해당 kind의 PDF를 업로드. - `ERR_PDF_NOT_GENERATED` (그룹 B): `POST /books/{bookUid}/finalization` 호출 후 재시도. - `ERR_PDF_PENDING` (그룹 B): 일정 간격(예: 10초~수십 초) 후 재시도. 무한 폴링 금지. **본 응답에는 `Retry-After` 헤더가 포함되지 않으므로** 클라이언트가 자체 대기 간격을 결정해야 합니다. - `ERR_PDF_GENERATION_FAILED` (그룹 B): 클라이언트 자체 해결 불가 — 사용자에게 실패 안내 + 고객지원팀 문의. - `ERR_PDF_FILE_MISSING` (그룹 B): 서버 측 결함 — 고객지원팀 문의. URL은 응답 본문이 아닌 표지 업로드 응답의 `data.url` 필드를 통해 확인할 수 있으며, 호출 도메인(`api-sandbox.sweetbook.com` / `api.sweetbook.com`)이 자동 적용됩니다. #### Request 예시 **Request — 파일로 저장** ```bash curl -o cover.pdf \\ 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **출처별 errorCode 분기**: `creationType`에 따라 그룹 A(`PDF_UPLOAD`·MIX 내지 — [404 ERR_PDF_NOT_UPLOADED](/docs/operations/errors#ERR_PDF_NOT_UPLOADED)) / 그룹 B(TEMPLATE·MIX 표지 — [409 ERR_PDF_NOT_GENERATED](/docs/operations/errors#ERR_PDF_NOT_GENERATED)·[ERR_PDF_PENDING](/docs/operations/errors#ERR_PDF_PENDING)·[422 ERR_PDF_GENERATION_FAILED](/docs/operations/errors#ERR_PDF_GENERATION_FAILED)·[500 ERR_PDF_FILE_MISSING](/docs/operations/errors#ERR_PDF_FILE_MISSING)). 응답 예시·처리 가이드는 [에러 코드 & 트러블슈팅 — PDF 다운로드 분기](/docs/operations/errors#pdf-download-branching)를 참조하세요. ## 내지 PDF 내지 PDF는 책 생성 시 지정한 `pageCount`와 페이지 수가 정확히 일치해야 합니다. 표지와 마찬가지로 **신규 등록(POST)**, **교체(PUT)**, **다운로드(GET)** 세 엔드포인트를 제공하며, 응답 본문 구조는 표지와 동일합니다. > ℹ️ `creationType="MIX_COVER_TEMPLATE"` 책의 내지는 PDF 업로드를 사용합니다 (표지는 템플릿). > 따라서 이 섹션의 내지 엔드포인트는 `PDF_UPLOAD`·`MIX_COVER_TEMPLATE` 두 방식 모두에서 사용됩니다. ### 공통 응답 필드 (POST · PUT) 구조는 [`표지 PDF의 공통 응답 필드`](#cover-pdf-common-fields)와 동일합니다 (`bookUid / kind / valid / pageMeta / pdfSizeMm / messages / warnings / bookStatus / url`). 내지의 경우 `kind` 값이 `"contents"`이고, 업로드 PDF의 페이지 수는 책 생성 시 지정한 `pageCount`와 정확히 일치해야 합니다 (불일치 시 400). ### HTTP 상태 코드 (POST · PUT 공통) 표지와 동일합니다. POST 신규는 201 Created (이미 등록 시 409), PUT 교체는 200 OK (미등록 시 404, 제작 확정 후 400). **환경(Sandbox/Live) 불일치 시 400**도 표지와 동일하게 적용됩니다 — 표지 섹션의 환경 불일치 안내 박스를 참고하세요. ### 내지 PDF 신규 등록 POST `/books/{bookUid}/pdf-contents` 내지 PDF를 최초로 등록합니다. 책 생성 시 지정한 `pageCount`와 PDF의 실제 페이지 수가 정확히 일치하지 않으면 400으로 차단됩니다. #### Request — multipart/form-data | 필드 | 위치 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | --- | | `bookUid` | path | string | O | 대상 책 UID | | `file` | form-data | file (application/pdf) | O | 내지 PDF 파일 (페이지 수 = 책의 `pageCount`) | #### 경고(warnings) 발생 조건 — 업로드는 성공, 주문 시 차단 업로드 자체는 성공하지만 응답의 `warnings` 배열에 메시지가 포함되며, 이후 주문 생성 시 400으로 차단됩니다. - 페이지 수가 판형의 `pageMin` 미만 - 페이지 수가 판형의 `pageMax` 초과 - 페이지 수가 판형의 `pageIncrement`의 배수가 아님 #### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcontents24.pdf;type=application/pdf' ``` #### Response 예시 **Response (201 Created)** ```json { "success": true, "message": "contents PDF가 저장되었습니다", "data": { "bookUid": "bk_2h4L********", "kind": "contents", "valid": true, "pdfSizeMm": { "width": 249, "height": 254 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_2h4L********/pdf-contents", "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true } } } ``` **pageMeta.currentPageCount**는 내지 PDF 실제 페이지 수를 반영합니다. 기존 `pageCount` 필드는 v1.2에서 제거됐으며 `pageMeta.currentPageCount`로 대체됐습니다. **주요 에러**: `400 ERR_VALIDATION_FAILED` 페이지 수 불일치 (`errors[0]`: `"내지 PDF 페이지수 불일치: 올바른 페이지수 24p, 업로드된 페이지수 30p"`. LAYFLAT 제본은 책 페이지 환산값이 괄호로 부가) · `409 ERR_CONFLICT` 이미 등록됨 (`errors[0]`: `"내지 PDF가 이미 등록되어 있습니다. 교체하려면 PUT 메서드를 사용하세요."`). 응답 shape 상세는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ### 내지 PDF 교체 PUT `/books/{bookUid}/pdf-contents` 등록된 내지 PDF를 교체합니다. 표지 교체와 동일하게 제작 확정(CONFIRMED) 이후에는 차단됩니다. 교체 시에도 책 생성 시 지정한 `pageCount`와 일치해야 합니다 (책의 `pageCount`는 변경되지 않음). > ⚠️ 내지 페이지 수를 바꾸려면 책의 `pageCount`를 변경해야 합니다. 그러나 `pageCount`는 책 생성 시 확정되며 이후 수정 API가 제공되지 않습니다 — 페이지 수가 다른 책은 새로 생성하세요. #### Request — multipart/form-data 신규 등록(POST)과 동일한 파라미터를 사용합니다. #### Request 예시 **Request** ```bash curl -X PUT 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@hardcontents24-v2.pdf;type=application/pdf' ``` #### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "contents PDF가 교체되었습니다", "data": { "bookUid": "bk_2h4L********", "kind": "contents", "valid": true, "pdfSizeMm": { "width": 249, "height": 254 }, "messages": [], "warnings": [], "bookStatus": 1, "url": "https://api-sandbox.sweetbook.com/v1/books/bk_2h4L********/pdf-contents", "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true } } } ``` **404 ERR_NOT_FOUND**: 등록되지 않은 상태에서 PUT 호출 — `errors[0]`: `"내지 PDF가 등록되어 있지 않습니다. 신규 등록은 POST 메서드를 사용하세요."` 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_NOT_FOUND](/docs/operations/errors#ERR_NOT_FOUND)를 참조하세요. ### 내지 PDF 다운로드 GET `/books/{bookUid}/pdf-contents` 저장된 내지 PDF를 다운로드합니다. 정상 응답은 JSON이 아닌 `application/pdf` 바이너리 스트림입니다. > ℹ️ 내지 PDF는 **PDF_UPLOAD**·**MIX_COVER_TEMPLATE** 두 방식 모두 사용자 업로드(그룹 A)이므로 응답 분기는 동일합니다 — 업로드된 파일 존재 여부만 검사하여 200 또는 404를 반환합니다. #### 응답 매트릭스 (그룹 A) | 파일 상태 | HTTP | 응답 형식 | errorCode | | --- | --- | --- | --- | | 디스크에 존재 | 200 | `application/pdf` 바이너리 스트림 | — | | 디스크에 부재 (업로드 미완) | 404 | JSON | `ERR_PDF_NOT_UPLOADED` | #### 공통 사전 검증 | 케이스 | HTTP | errorCode | | --- | --- | --- | | 책 미존재 | 404 | `ERR_NOT_FOUND` | | 본인 소유가 아닌 책 | 403 | `ERR_FORBIDDEN` | | 환경 불일치 | 403 | `ERR_ENV_MISMATCH` | | kind 경로 오류 | 400 | `ERR_MALFORMED_REQUEST` | | 책의 `creationType`이 PDF 조회를 지원하지 않음 | 400 | `ERR_CREATION_TYPE_UNSUPPORTED` | #### Request 예시 **Request — 파일로 저장** ```bash curl -o contents.pdf \\ 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **404 ERR_PDF_NOT_UPLOADED**: 내지 PDF 미업로드 (그룹 A) — `errors[0]`: `"PDF 파일이 업로드되지 않았습니다."` 응답 shape·다른 errorCode·처리 가이드는 [에러 코드 & 트러블슈팅 — ERR_PDF_NOT_UPLOADED](/docs/operations/errors#ERR_PDF_NOT_UPLOADED)를 참조하세요. ## 책 최종화 POST `/books/{bookUid}/finalization` 표지·내지 PDF 업로드를 마친 뒤 이 엔드포인트를 호출해 책을 최종화합니다. 호출 시 책의 `status`가 `draft`에서 `finalized`로 전환되어 주문 가능 상태가 됩니다. > ⚠️ **사전 조건**: 표지·내지 PDF가 **모두 업로드**되어 있어야 합니다. 한쪽이라도 누락되면 400 에러가 반환됩니다. > > > **전환 결과**: 호출 성공 시 `status`가 `draft` → `finalized`로, `pdfStatus`가 `null` → `2`(COMPLETED)로 전환됩니다. > ℹ️ **주문 시 PDF_READY 자동 승격**: PDF 방식으로 finalize된 책은 PDF가 이미 준비된 상태이므로, 주문 생성 시 일반 흐름의 `PAID`(20) 단계를 거치지 않고 즉시 `PDF_READY`(25)로 진입합니다. 자세한 주문 상태 흐름은 [주문 상태 흐름](/docs/operations/order-status)을 참고하세요. ### Request 요청 본문은 비어 있습니다. 다만 IIS 환경 특성상 `Content-Length: 0` 헤더가 필요합니다 (없으면 411 Length Required). ### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/finalization' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Length: 0' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `result` | string | 처리 결과. `"created"`(첫 finalize 성공) / `"updated"`(이미 finalized 상태에서 멱등 응답) | | `pageMeta` | object | 페이지 제약 메타. `currentPageCount`(책의 내지 페이지 수, PDF 방식은 책 생성 시 지정한 값과 동일) + `pageMin`·`pageMax`·`pageIncrement`·`isValid` 필드 포함 | | `finalizedAt` | datetime | 최종화 완료 시각 (ISO 8601) | 이미 FINALIZED 상태에서 다시 호출하면 200 OK + `message: "이미 최종화된 책입니다"` + `result: "updated"`로 멱등 응답합니다 (실제 상태 재변경은 일어나지 않음). ### Response 예시 **Response (201 Created — 첫 finalize)** ```json { "success": true, "message": "책 최종화 완료", "data": { "result": "created", "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true }, "finalizedAt": "2026-04-22T07:46:24.099Z" } } ``` **Response (200 OK — 이미 finalized 상태에서 재호출)** ```json { "success": true, "message": "이미 최종화된 책입니다", "data": { "result": "updated", "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true }, "finalizedAt": "2026-04-22T07:46:24.099Z" } } ``` ### 전제조건별 에러코드 매핑 finalize 호출 시 전제조건을 충족하지 못하면 아래 표대로 `errorCode` + `fieldErrors`가 반환됩니다. 클라이언트는 `fieldErrors[].field`·`fieldErrors[].constraint`로 누락 항목을 식별해 사용자에게 안내하세요. | 전제조건 | `errorCode` | `fieldErrors[].field` | `constraint` | | --- | --- | --- | --- | | 책이 DRAFT 상태가 아님 (공통) | `ERR_FINALIZE_PREREQ_UNMET` | `status` | `enum` | | 표지 PDF 미업로드 (`PDF_UPLOAD` 전용) | `ERR_FINALIZE_PREREQ_UNMET` | `coverPdf` | `required` | | 내지 PDF 미업로드 (`PDF_UPLOAD` · `MIX_COVER_TEMPLATE` 공통) | `ERR_FINALIZE_PREREQ_UNMET` | `contentsPdf` | `required` | | 페이지 수 < `pageMin` | `ERR_INSUFFICIENT_PAGES` | `pageCount` | `min` | | 페이지 수 > `pageMax` | `ERR_PAGECOUNT_INVALID` | `pageCount` | `max` | | 페이지 증분 규칙 위반 | `ERR_PAGECOUNT_INVALID` | `pageCount` | `increment` | 여러 전제조건이 동시에 깨지면 `fieldErrors`에 항목들이 함께 반환됩니다(예: 표지·내지 PDF 둘 다 없으면 두 항목 모두 포함). 응답 예시는 위 [페이지 제약 위반 에러](#page-constraint-errors) 섹션과 케이스 C(`ERR_FINALIZE_PREREQ_UNMET` 다중 필드)를 참조하세요. > ⚠️ **`pageMeta.isValid` 사용 시 주의:** > > - `isValid`는 **페이지 수 제약**(`pageMin`·`pageMax`·`pageIncrement`)**만** 판정합니다. > - 표지·내지 PDF 업로드 여부, 책 상태(DRAFT 여부) 등 다른 전제조건은 finalize 호출 시 별도로 검증되므로 `isValid: true`여도 `ERR_FINALIZE_PREREQ_UNMET`이 반환될 수 있습니다. > - **finalize 가능 여부 최종 확정은 `POST /finalization` 호출 결과로만 가능**합니다. `isValid`는 사전 UI 안내(예: "아직 N페이지 부족") 용도로만 사용하세요. ### HTTP 상태 코드 | 코드 | `errorCode` | 설명 | | --- | --- | --- | | 200 OK | — | 이미 `finalized` 상태의 책에 재호출한 경우 (멱등 응답, `message: "이미 최종화된 책입니다"`, `result: "updated"`) | | 201 Created | — | 최종화 신규 성공 | | 400 Bad Request | `ERR_FINALIZE_PREREQ_UNMET` / `ERR_INSUFFICIENT_PAGES` / `ERR_PAGECOUNT_INVALID` | 전제조건 미충족 (위 매핑 표 참고) | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 책 없음 | | 411 Length Required | — | `Content-Length: 0` 헤더 누락 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ### book.pdfStatus 값 책 조회(`GET /books`) 응답에 포함되는 `pdfStatus` 필드는 PDF 처리 상태를 나타내는 정수값입니다. 파트너는 일반적으로 책의 `status` 필드만 참조하면 충분하며, `pdfStatus`는 PDF 준비 완료 여부를 직접 확인하고 싶을 때 부가 정보로 활용할 수 있습니다. | 값 | 의미 | 발생 시점 | | --- | --- | --- | | `null` | 미생성/미업로드 (초기 상태) | 책 생성 직후 또는 표지·내지 PDF만 업로드한 상태 (finalize 미호출) | | `2` | COMPLETED (PDF 준비 완료) | finalize 호출 후. PDF 방식은 즉시 `2`로 전환됨 | | `9` | FAILED (처리 실패) | 처리 실패 시. PDF 방식에서는 일반적으로 발생하지 않음 (사용자가 PDF를 직접 제공하므로 렌더링 실패 경로가 없음) | **400 ERR_FINALIZE_PREREQ_UNMET**: 표지/내지 PDF 미업로드 등 전제조건 미충족. 위의 [전제조건별 에러코드 매핑 표](#prereq-mapping)가 정확한 매핑을 제공하며, 표지만 업로드되고 내지가 없으면 `fieldErrors`에 `contentsPdf`만, 반대면 `coverPdf`만 포함됩니다. 응답 예시(다중 fieldErrors)와 처리 가이드는 [에러 코드 & 트러블슈팅 — ERR_FINALIZE_PREREQ_UNMET](/docs/operations/errors#ERR_FINALIZE_PREREQ_UNMET)를 참조하세요. ## 책 목록 조회 GET `/books` 파트너가 생성한 책 목록을 조회합니다. 다양한 쿼리 파라미터로 필터링할 수 있으며, 페이지네이션을 지원합니다. 응답에는 PDF 방식·템플릿 방식 책이 모두 포함되며, `creationType` 필드로 구분할 수 있습니다. ### Query 파라미터 | 파라미터 | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | — | 특정 책 UID로 단건 조회 | | `status` | string | — | 책 상태 필터. `draft` / `finalized` / `deleted`(일반 파트너 조회 결과에서는 삭제된 책이 자동 제외되므로 빈 결과) | | `pdfStatus` | string | — | PDF 상태 필터. 숫자 값(`2` COMPLETED / `9` FAILED) 또는 `null` 문자열(PDF 미생성 책) 허용. PDF 방식 책 중 finalize 완료된 것만 조회 시 `pdfStatus=2`(COMPLETED) 활용 | | `specProfileUid` | string | — | SpecProfile UID로 필터 | | `createdFrom` | string | — | 생성일 시작 (ISO 8601, 예: `2026-01-01`) | | `createdTo` | string | — | 생성일 종료 (ISO 8601) | | `limit` | integer | 20 | 조회할 항목 수 (1~100) | | `offset` | integer | 0 | 건너뛸 항목 수 (≥0) | 페이지네이션 동작과 응답 구조(`data.books` 배열 + `data.pagination` 객체)는 [페이지네이션 가이드](/docs/operations/pagination)를 참고하세요. ### Request 예시 **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books?limit=5' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Request — finalize 완료된 PDF 책만 조회** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books?status=finalized&pdfStatus=2&limit=20' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 책 고유 UID | | `accountUid` | string | 책 소유 계정 UID | | `title` | string | 책 제목 | | `author` | string | null | 저자명 (선택) | | `status` | string | 책 상태 — enum 문자열로 반환. `"draft"`(작성 중, 내부값 1) / `"finalized"`(최종화 완료, 내부값 2) / `"deleted"`(삭제됨, 내부값 9). PDF 업로드 응답의 `bookStatus`는 같은 값을 정수로 반환합니다 | | `pageCount` | integer | 내지 페이지 수 (PDF 방식은 책 생성 시 지정한 값, TEMPLATE은 자동 계산) | | `bookSpecUid` | string | 판형 UID | | `specProfileUid` | string | null | SpecProfile UID | | `creationType` | string | `TEMPLATE` / `PDF_UPLOAD` / `MIX_COVER_TEMPLATE` | | `createdAt` | datetime | 책 생성 시각 (ISO 8601) | | `updatedAt` | datetime | 마지막 수정 시각 | | `externalRef` | string | null | 파트너 외부 참조 식별자 | | `isTest` | boolean | Sandbox 환경 책 여부 | | `pdfStatus` | integer | null | PDF 처리 상태 (정수). `null`(미생성) / `2`(COMPLETED, 준비 완료) / `9`(FAILED, 실패). 자세한 의미는 위 **책 최종화 → book.pdfStatus 값** 표 참고 | | `pdfCreatedAt` | datetime | null | PDF 파일이 마지막으로 갱신된 시각 (업로드/교체/finalize 시 갱신) | | `pdfRequestedAt` | datetime | null | PDF 요청 시각 (PDF 방식은 일반적으로 `null`) | | `thumbnailStatus` | integer | null | 책 페이지 썸네일 처리 상태. `null`(NONE, 미생성) / `1`(PENDING, 생성 진행 중) / `2`(COMPLETED, 생성 완료) / `9`(FAILED, 생성 실패) | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "성공", "data": [ { "bookUid": "bk_2h4L********", "accountUid": "u_b4b9********************", "title": "PDF 업로드 샘플", "author": null, "status": "finalized", "pageCount": 24, "bookSpecUid": "SQUAREBOOK_HC", "specProfileUid": null, "creationType": "PDF_UPLOAD", "createdAt": "2026-04-22T07:45:05.759Z", "updatedAt": "2026-04-22T07:46:24.099Z", "externalRef": "PARTNER-ORDER-001", "isTest": true, "pdfStatus": 2, "pdfCreatedAt": "2026-04-22T07:46:24.000Z", "pdfRequestedAt": null, "thumbnailStatus": null }, { "bookUid": "bk_5EdC********", "title": "DRAFT 상태 PDF 책", "status": "draft", "creationType": "PDF_UPLOAD", "pageCount": 24, "pdfStatus": null, "pdfCreatedAt": null /* ... 나머지 필드 생략 ... */ } ], "pagination": { "total": 15, "limit": 5, "offset": 0, "hasNext": true } } ``` ### HTTP 상태 코드 | 코드 | 설명 | | --- | --- | | 200 OK | 조회 성공 | | 400 Bad Request | `limit`/`offset` 범위 위반 등 파라미터 오류 | | 401 Unauthorized | 인증 실패 | | 500 Internal Server Error | 서버 오류 | ## 책 단건 조회 GET `/books/{bookUid}` 특정 책의 상세 정보를 조회합니다. v1.2에 신설된 RESTful 단건 조회 엔드포인트로, 편집 화면 진입이나 현재 페이지 수 제약 확인에 사용합니다. 응답에 `pageMeta`가 포함되어 "아직 N페이지 부족" 같은 힌트를 UI에 즉시 반영할 수 있습니다. ### Path 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | O | 조회할 책 UID | ### Request 예시 **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 책 UID | | `accountUid` | string | 책을 생성한 계정의 UID (파트너 본인 계정) | | `title` | string | 책 제목 | | `bookSpecUid` | string | 책 규격 UID | | `bookSpecName` | string | 규격 이름 (예: "A5 소프트커버 포토북") | | `specProfileUid` | string | null | SpecProfile UID (사용 시) | | `creationType` | string | `TEMPLATE` / `PDF_UPLOAD` / `MIX_COVER_TEMPLATE` | | `status` | integer | `1`=작성 중(DRAFT) / `2`=최종화 완료(FINALIZED). `status`가 `1`인 책은 주문을 생성할 수 없으며, `POST /books/{bookUid}/finalization`을 호출해 `2`로 전이해야 합니다. **목록 조회(`GET /books`)는 동일 값을 문자열**(`"draft"` / `"finalized"`)**로 반환**합니다 | | `coverTemplateUid` | string | null | 템플릿 방식에서 바인딩된 표지 템플릿 UID. PDF 방식은 `null` | | `externalRef` | string | null | 파트너 외부 참조 식별자 | | `isTest` | boolean | Sandbox 환경의 책이면 `true` | | `pageMeta` | object | 페이지 제약 메타. `currentPageCount`·`pageMin`·`pageMax`·`pageIncrement`·`isValid` 5개 필드 | | `createdAt` | datetime | 생성 시각 (ISO 8601) | | `updatedAt` | datetime | 최종 수정 시각 (ISO 8601) | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "성공", "data": { "bookUid": "bk_2h4L********", "accountUid": "u_b4b9********************", "title": "PDF 업로드 샘플", "bookSpecUid": "SQUAREBOOK_HC", "bookSpecName": "고화질 스퀘어북 (하드커버)", "specProfileUid": null, "creationType": "PDF_UPLOAD", "status": 1, "coverTemplateUid": null, "externalRef": "PARTNER-ORDER-001", "isTest": true, "pageMeta": { "currentPageCount": 24, "pageMin": 24, "pageMax": 200, "pageIncrement": 2, "isValid": true }, "createdAt": "2026-04-22T03:00:00.000Z", "updatedAt": "2026-04-22T04:00:00.000Z" } } ``` ### HTTP 상태 코드 | 상태 코드 | `errorCode` | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 403 Forbidden | `ERR_FORBIDDEN` | 다른 파트너의 책 접근 시도 | | 403 Forbidden | `ERR_ENV_MISMATCH` | Sandbox 도메인에서 Live 책 조회(또는 반대) | | 404 Not Found | `ERR_NOT_FOUND` | 책 UID가 존재하지 않음 | ### 단건 조회 vs 목록 조회 (`bookUid` 필터) 차이 기존 목록 조회의 `bookUid` 필터(`GET /books?bookUid=...`)와 본 단건 조회는 의미·응답 타입·권한 처리가 다릅니다. 용도에 맞게 선택하세요. | 항목 | `GET /books/{uid}` (단건) | `GET /books?bookUid=...` (목록 필터) | | --- | --- | --- | | 의미론 | RESTful 리소스 단건 조회 | 목록 조회 + `bookUid` 동등 필터 (결과 0~1건) | | 응답 `data` 타입 | 객체 | 배열 + `pagination` 메타 | | 응답 필드 | 상세 필드 + `pageMeta`, `bookSpecName`, `coverTemplateUid` 등 편집 화면용 풀셋 | 목록용 요약 필드 (`pageCount` 유지, `pageMeta` 없음) | | 결과 없을 때 | 404 `ERR_NOT_FOUND` | 200 + 빈 배열 | | 다른 파트너의 책 | 403 `ERR_FORBIDDEN` | 200 + 빈 배열 (필터 결과) | | 환경 불일치 (sandbox/live) | 403 `ERR_ENV_MISMATCH` | 빈 배열 (필터에 환경 적용) | | `status` 필드 타입 | 숫자 (`1`/`2`) | 문자열 (`"draft"`/`"finalized"`) | | 주 용도 | 책 편집 페이지 진입, 상세 패널 렌더링, finalize 전 상태 확인 | 목록/검색 UI, 페이지네이션, 여러 조건 조합 필터링 | - 책 UID를 알고 있고 **상세 정보(특히 `pageMeta`)가 필요**하면 → 단건 조회 - 존재 여부만 빠르게 확인하거나 여러 조건과 조합해야 하면 → 목록 조회 - **없는 책과 권한 없는 책을 명확히 구별해야 하면** → 단건 조회 (목록은 둘 다 빈 배열로 동일) **404 ERR_NOT_FOUND**: 존재하지 않는 `bookUid` — `errors[0]`: `"책을 찾을 수 없습니다: bk_xxx"`. 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_NOT_FOUND](/docs/operations/errors#ERR_NOT_FOUND)를 참조하세요. ## 페이지 제약 위반 에러 내지 PDF 업로드·최종화·내지 템플릿 추가 시 페이지 제약을 위반하면 아래 3종 에러 중 하나가 반환됩니다. `fieldErrors[0].constraint`로 어떤 제약이 위반됐는지 분기할 수 있습니다. 전체 에러 체계 상세는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참고하세요. - `ERR_INSUFFICIENT_PAGES` (`constraint: "min"`) — 최소 페이지 미달 - `ERR_PAGECOUNT_INVALID` (`constraint: "max"`) — 최대 페이지 초과 - `ERR_PAGECOUNT_INVALID` (`constraint: "increment"`) — 증분 규칙 위반. `requiredValue`는 `{min, increment}` 객체 형태 응답 예시(`fieldErrors` 배열에 `currentValue`·`requiredValue`·`constraint` 포함)는 errors 페이지의 각 errorCode 카드를 참조하세요: [ERR_INSUFFICIENT_PAGES](/docs/operations/errors#ERR_INSUFFICIENT_PAGES), [ERR_PAGECOUNT_INVALID](/docs/operations/errors#ERR_PAGECOUNT_INVALID). ## 책 삭제 DELETE `/books/{bookUid}` 책을 삭제 처리합니다. **물리적 삭제가 아닌 소프트 삭제**(상태값 변경)로 처리되어, 이후 목록 조회 결과에서 제외됩니다. 업로드한 PDF 파일은 별도 정리되지 않고 보관 상태로 남으며, 책 자체에 대한 추가 작업(업로드/교체/finalize/주문)은 모두 차단됩니다. > ℹ️ **멱등 동작**: 이미 삭제된 책에 다시 호출해도 200 OK를 반환합니다 (재실패하지 않음). > > > **상태 제약 없음**: `draft`·`finalized` 어느 상태에서도 삭제 가능합니다 (단 본인 소유 책에 한함). ### Path 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | O | 삭제할 책 UID | ### Request 예시 **Request** ```bash curl -X DELETE 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 삭제된 책 UID | | `status` | string | 삭제 처리 결과 (`"deleted"`) | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "책이 삭제되었습니다", "data": { "bookUid": "bk_2h4L********", "status": "deleted" } } ``` ### HTTP 상태 코드 | 코드 | 설명 | | --- | --- | | 200 OK | 삭제 성공 (이미 삭제된 책에 대한 재호출 포함) | | 400 Bad Request | `bookUid` 형식 오류 | | 401 Unauthorized | 인증 실패 | | 403 Forbidden | 본인 소유가 아닌 책 | | 404 Not Found | 책 없음 | | 500 Internal Server Error | 서버 오류 | **404 ERR_NOT_FOUND**: 존재하지 않는 `bookUid` — 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_NOT_FOUND](/docs/operations/errors#ERR_NOT_FOUND)를 참조하세요. ## 관련 문서 - [PDF 업로드 시나리오 — — 책 생성부터 주문까지 단계별 흐름](/docs/guides/scenario-pdf) - [혼합 방식 시나리오 — — 표지=템플릿, 내지=PDF 흐름](/docs/guides/scenario-mix) - [BookSpecs API — — 판형 정보 (PDF 규격 포함) 조회](/docs/api/book-specs) - [Orders API — — 책 주문 생성, 상태 추적, 취소](/docs/api/orders) --- # Books API — 템플릿 기반 SweetBook 템플릿에 이미지·텍스트를 바인딩하여 책을 생성하는 방식의 API 레퍼런스입니다. 표지·내지 페이지를 템플릿 단위로 추가하고, 명시적인 최종화(finalize) 호출로 주문 가능 상태로 전환합니다. > ℹ️ **PDF 직접 업로드 방식**(`creationType="PDF_UPLOAD"` / `"MIX_COVER_TEMPLATE"`)은 [Books — PDF 기반](/docs/api/books-pdf) 페이지를 참고하세요. > 실제 호출 흐름을 단계별 따라 하기로 보려면 [템플릿 기반 시나리오](/docs/guides/scenario-template)를 참고하세요. > ℹ️ 모든 Books API는 인증이 필요합니다. API Key를 `Authorization: Bearer {YOUR_API_KEY}` 헤더로 제공하세요. 자세한 인증 안내는 [인증 가이드](/docs/authentication)를 참고하세요. ## 이 방식이 적합한 경우 - SweetBook이 제공하는 표지·내지 템플릿을 활용하고 싶은 경우 - 이미지·텍스트만 입력하고 레이아웃은 템플릿에 맡기는 경우 - 여러 페이지를 다양한 템플릿(갤러리/베이스 레이어/간지/발행면 등)으로 조합하고 싶은 경우 ### 지원 `creationType` | `creationType` | 표지 | 내지 | 비고 | | --- | --- | --- | --- | | `TEMPLATE` | 템플릿 바인딩 | 템플릿 바인딩 | 이 페이지의 기본 흐름 | | `MIX_COVER_TEMPLATE` | 템플릿 바인딩 | PDF 직접 업로드 | 표지만 이 페이지의 `POST /books/{bookUid}/cover` 사용. 내지 PDF 업로드는 [Books — PDF 기반](/docs/api/books-pdf)의 `POST /pdf-contents` 사용 | `PDF_UPLOAD` 방식은 표지·내지 모두 PDF 직접 업로드로 진행하며, 이 페이지의 템플릿 엔드포인트(`/cover`, `/contents`, `/photos`)는 사용할 수 없습니다. [Books — PDF 기반](/docs/api/books-pdf)을 참고하세요. ## 책 생성 POST `/books` 새로운 책을 생성합니다. 생성 직후 `status="draft"` 상태이며, 이후 표지·내지 추가와 명시적인 최종화 단계를 거쳐 주문 가능 상태로 진입합니다. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `title` | string | O | 책 제목 (1~255자) | | `bookSpecUid` | string | O | 판형 UID (예: `SQUAREBOOK_HC`). [BookSpecs API](/docs/api/book-specs)로 조회 | | `creationType` | string | O | `"TEMPLATE"` 또는 `"MIX_COVER_TEMPLATE"`. 그 외 값은 400 에러 | | `pageCount` | integer | 조건부 | `TEMPLATE`: **불필요**. 요청에 포함해도 서버에서 무시되며(에러 없음), 실제 `pageCount`는 `0`으로 초기화된 뒤 콘텐츠 추가에 따라 자동 산출됩니다. `MIX_COVER_TEMPLATE`: **필수** (내지 PDF 페이지 수) | | `specProfileUid` | string | — | SpecProfile UID (선택, 일반적으로 미사용). 사용자별 판형 프로파일이 사전 등록된 경우에만 사용. 제공 시 해당 프로필과 연결, 유효하지 않으면 400 | | `externalRef` | string | — | 파트너 외부 참조 식별자 (최대 100자). 파트너 시스템의 고유 ID 매핑용 | > ℹ️ **이 단계에서는 빈 책만 생성**됩니다. 응답으로 받은 `bookUid`를 사용해 표지(`POST /cover`)·내지(`POST /contents`) 추가와 최종화(`POST /finalization`)를 순차적으로 진행해야 주문 가능한 상태(`finalized`)가 됩니다. > ℹ️ **Idempotency-Key 지원**: `POST /books`는 `Idempotency-Key` 헤더를 지원합니다. 동일한 키로 재요청해도 책이 중복 생성되지 않으므로, 네트워크 오류 등 재시도 상황에 활용하세요. > 재시도가 캐시된 응답으로 처리된 경우 응답 헤더에 `X-Idempotent-Replayed: true`가 포함됩니다. > 자세한 동작은 [멱등성 가이드](/docs/operations/idempotency)를 참고하세요. ### Request 예시 **Request body (TEMPLATE)** ```json { "title": "일기장 샘플", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "TEMPLATE", "externalRef": "PARTNER-ORDER-001" } ``` **Request body (MIX_COVER_TEMPLATE)** ```json { "title": "혼합 방식 샘플", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "MIX_COVER_TEMPLATE", "pageCount": 24, "externalRef": "PARTNER-ORDER-002" } ``` **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Idempotency-Key: unique-request-id-001' \\ -H 'Content-Type: application/json' \\ -d '{ "title": "일기장 샘플", "bookSpecUid": "SQUAREBOOK_HC", "creationType": "TEMPLATE", "externalRef": "PARTNER-ORDER-001" }' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 생성된 책의 UID | | `pageMeta` | object | 페이지 제약 메타. `currentPageCount`·`pageMin`·`pageMax`·`pageIncrement`·`isValid` 5개 필드 | ### Response 예시 **Response (201 Created)** ```json { "success": true, "message": "책 생성 완료", "data": { "bookUid": "bk_3k2S********", "pageMeta": { "currentPageCount": 0, "pageMin": 24, "pageMax": 200, "pageIncrement": 1, "isValid": false } } } ``` **pageMeta:** TEMPLATE 방식은 생성 직후 `currentPageCount=0`, `isValid=false`로 시작합니다. 콘텐츠 추가(`POST /books/{bookUid}/contents`) 호출에 따라 자동 증가합니다. `MIX_COVER_TEMPLATE`은 요청의 `pageCount` 값이 `currentPageCount`로 반영됩니다. `isValid`는 페이지 제약만 판정하며, finalize 가능 여부는 표지·내지 존재 여부 등 다른 조건도 포함되므로 `POST /finalization` 호출로 최종 확인하세요. ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 201 Created | — | 책 생성 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `title`/`bookSpecUid`/`creationType` 누락 또는 허용되지 않은 값, MIX 방식인데 `pageCount` 누락, 잘못된 `specProfileUid` 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 422 Unprocessable Entity | `ERR_IDEMPOTENCY_KEY_MISMATCH` | 동일 `Idempotency-Key`로 다른 본문 재요청 | | 429 Too Many Requests | `ERR_TOO_MANY_REQUESTS` | 요청 빈도 제한 초과 — [Rate Limiting 가이드](/docs/operations/rate-limiting) 참고 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 본 엔드포인트의 주요 검증 실패 메시지(`400 ERR_VALIDATION_FAILED`): - `creationType` 누락 또는 허용되지 않은 값 — `errors[0]`: `"유효하지 않은 creationType: <값> (허용: TEMPLATE, PDF_UPLOAD, MIX_COVER_TEMPLATE)"` - `MIX_COVER_TEMPLATE`인데 `pageCount` 누락 — `errors[0]`: `"creation_type=MIX_COVER_TEMPLATE는 pageCount(내지 페이지수)가 필수입니다."` `TEMPLATE`은 `pageCount`가 필요 없으므로 누락해도 에러가 발생하지 않으며, 값을 포함해도 서버에서 무시됩니다(실제 저장값은 `0`). 응답 6필드 shape, 다른 errorCode, 처리 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ## 표지 추가 POST `/books/{bookUid}/cover` 지정한 표지 템플릿에 이미지·텍스트를 바인딩하여 책의 표지를 생성합니다. 이미지는 위 **이미지 처리 → 이미지 전달 방식** 섹션의 3가지 방식 중 어느 것으로든 제공할 수 있고, 한 요청에서 혼용도 가능합니다. > ⚠️ **표지는 한 번만 등록 가능**합니다. 이미 표지가 등록된 책에 다시 호출하면 **400 에러**가 반환됩니다. 표지를 변경하려면 책을 새로 생성하세요. > ℹ️ **`creationType="MIX_COVER_TEMPLATE"` 책의 표지도 이 엔드포인트를 사용**합니다 (표지는 템플릿, 내지는 PDF). 단 `creationType="PDF_UPLOAD"` 책에는 사용할 수 없습니다 (그 경우 [Books — PDF 기반](/docs/api/books-pdf)의 `POST /pdf-cover` 사용). ### Request — multipart/form-data | 필드 | 위치 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | --- | | `bookUid` | path | string | O | 대상 책 UID | | `templateUid` | form | string | O | 표지 템플릿 UID. [Templates API](/docs/api/templates)에서 `templateKind=cover`로 조회 | | `parameters` | form | string (JSON) | — | 템플릿 파라미터 (텍스트 값, 이미지 URL, 서버 파일명, `$upload` 플레이스홀더 포함). 템플릿이 요구하는 변수에 따라 필수성 결정 | | (이미지 필드명) | form-data | file | — | 템플릿의 이미지 변수명(`$변수명$`)과 동일한 이름으로 multipart 파일 첨부 (예: `coverPhoto=@front.jpg`) | 템플릿의 변수 목록과 필수 여부는 [Templates API](/docs/api/templates)의 템플릿 상세 조회로 확인할 수 있습니다. 템플릿 구조와 바인딩 동작 원리는 [템플릿 구조와 바인딩](/docs/concepts/template-structure)을 참고하세요. > ℹ️ **Idempotency-Key 지원**: `Idempotency-Key` 헤더를 함께 보내면 동일한 키로 재요청해도 표지가 중복 추가되지 않습니다. 자세한 동작은 [멱등성 가이드](/docs/operations/idempotency)를 참고하세요. ### Request 예시 **Request — 파일 업로드 방식** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=79yjMH3qRPly' \\ -F 'coverPhoto=@front.jpg' \\ -F 'parameters={"title":"나의 하루 기록","dateRange":"26.01 - 26.04","coverPhoto":"$upload"}' ``` **Request — URL 방식** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=79yjMH3qRPly' \\ -F 'parameters={"title":"나의 하루 기록","coverPhoto":"https://example.com/front.jpg"}' ``` **Request — 사전 업로드 서버 파일명 방식** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=79yjMH3qRPly' \\ -F 'parameters={"title":"나의 하루 기록","coverPhoto":"photo250105143052123.JPG"}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `result` | string | 처리 결과. `"inserted"` 고정 | | `pageMeta` | object | 페이지 제약 메타 (참고용 — 표지는 카운트에 미포함) | ### Response 예시 **Response (201 Created)** ```json { "success": true, "message": "표지가 생성되었습니다", "data": { "result": "inserted", "pageMeta": { "currentPageCount": 0, "pageMin": 24, "pageMax": 200, "pageIncrement": 1, "isValid": false } } } ``` 표지는 내지 카운트(`currentPageCount`)에 포함되지 않으므로 표지 추가 후에도 값이 변하지 않습니다. `pageMeta`는 현재 내지 상태를 보고용으로 함께 노출됩니다. ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 201 Created | — | 표지 신규 등록 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` / `ERR_CREATION_TYPE_UNSUPPORTED` | `templateUid` 누락, 필수 이미지 파라미터 누락, `parameters` JSON 파싱 실패, 이미 표지 등록됨("이미 표지가 존재합니다"), `PDF_UPLOAD` 책에 사용 시 차단 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 책 또는 템플릿 없음 | | 429 Too Many Requests | `ERR_TOO_MANY_REQUESTS` | 업로드 Rate Limit 초과 — [Rate Limiting 가이드](/docs/operations/rate-limiting) 참고 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 본 엔드포인트의 주요 검증 실패 메시지(`400 ERR_VALIDATION_FAILED`): - 이미 표지가 등록됨 — `errors[0]`: `"이미 표지가 존재합니다"` - 필수 이미지 파라미터 누락 — `errors[0]`: `"필수 이미지 파라미터 '' (<설명>)이 제공되지 않았습니다. form-data에 파일을 첨부하거나 parameters에 URL 또는 서버 파일명을 제공해야 합니다."` 파라미터 이름(예: `coverPhoto`)과 설명(예: `앞표지 메인 사진`)은 템플릿에 따라 달라집니다. [Templates API](/docs/api/templates)에서 해당 템플릿의 필수 변수 목록을 먼저 확인하세요. 응답 6필드 shape, 다른 errorCode, 처리 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ## 콘텐츠(내지) 추가 POST `/books/{bookUid}/contents` 지정한 콘텐츠(내지) 템플릿에 이미지·텍스트를 바인딩하여 페이지를 추가합니다. 한 번 호출에 한 콘텐츠가 추가되며, 여러 페이지를 만들려면 반복 호출합니다. 이미지 전달 방식은 위 **이미지 처리 → 이미지 전달 방식** 섹션과 동일합니다. > ⚠️ **`creationType="PDF_UPLOAD"` · `"MIX_COVER_TEMPLATE"` 책에는 이 엔드포인트를 사용할 수 없습니다.** 두 방식은 내지를 PDF로 업로드합니다 ([Books — PDF 기반](/docs/api/books-pdf)의 `POST /pdf-contents` 사용). ### Request — multipart/form-data | 필드 | 위치 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | --- | | `bookUid` | path | string | O | 대상 책 UID | | `templateUid` | form | string | O | 콘텐츠/간지/발행면 템플릿 UID. [Templates API](/docs/api/templates)에서 `templateKind=content/divider/publish`로 조회 | | `parameters` | form | string (JSON) | — | 템플릿 파라미터. 갤러리 등 다수 이미지를 받는 변수는 배열로 전달 가능 (예: `"galleryPhotos": ["url1", "url2"]`) | | `breakBefore` | query | string | — | 페이지 나누기 방식. `page`(새 페이지) / `column`(새 단) / `none`(이어붙기). 기본값은 템플릿 종류에 따라 다름 (**content: none**, **divider/publish: page**). 위 외 값은 400 에러 | | (이미지 필드명) | form-data | file | — | 템플릿의 이미지 변수명(`$변수명$`)과 동일한 이름으로 multipart 파일 첨부 | 템플릿 종류와 변수 정보는 [Templates API](/docs/api/templates)에서, 레이아웃 동작 원리(이어붙기 vs 새 페이지)는 [레이아웃 동작 원리](/docs/concepts/layout-principles)를 참고하세요. 갤러리 템플릿(다수 이미지 배치)은 [갤러리 템플릿](/docs/concepts/gallery)을 참고하세요. > ℹ️ **Idempotency-Key 지원**: 콘텐츠 추가도 `Idempotency-Key` 헤더로 중복 추가를 방지할 수 있습니다. 자세한 동작은 [멱등성 가이드](/docs/operations/idempotency)를 참고하세요. ### Request 예시 **Request — 파일 업로드 방식 (단일 이미지)** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/contents?breakBefore=page' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=46VqZhVNOfAp' \\ -F 'photo=@image1.jpg' \\ -F 'parameters={"monthNum":"04","dayNum":"14","diaryText":"오늘의 일기","photo":"$upload"}' ``` **Request — URL 방식** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=46VqZhVNOfAp' \\ -F 'parameters={"monthNum":"04","dayNum":"14","diaryText":"오늘의 일기","photo":"https://example.com/photo.jpg"}' ``` **Request — 갤러리(배열) URL 방식** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid={GALLERY_TEMPLATE_UID}' \\ -F 'parameters={"date":"2026-04-22","galleryPhotos":["https://example.com/1.jpg","https://example.com/2.jpg","https://example.com/3.jpg"]}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `result` | string | 처리 결과. `"inserted"`(새 페이지 신규 추가) / `"updated"`(같은 페이지 범위 내 기존 콘텐츠가 있어 left→right로 이동하거나 교체되어 저장됨) | | `breakBefore` | string | 실제로 적용된 페이지 나누기 값 (요청 값 또는 템플릿별 기본값) | | `pageNum` | integer | 이번 콘텐츠가 배치된 페이지 번호 (1부터 시작) | | `pageSide` | string | 페이지 방향. `"left"` / `"right"` | | `pageMeta` | object | 페이지 제약 메타 객체. `currentPageCount`(누적 내지 페이지 수 — 제본 방식에 따라 계산, 같은 `pageNum` 안에서 left/right만 채우는 호출은 증가하지 않을 수 있음. 산출 방식 상세는 [레이아웃 동작 원리](/docs/concepts/layout-principles) 참고) + `pageMin`·`pageMax`·`pageIncrement`·`isValid` 필드 포함 | ### Response 예시 **Response (201 Created — 새 페이지 추가)** ```json { "success": true, "message": "내지가 생성되었습니다", "data": { "result": "inserted", "breakBefore": "page", "pageNum": 1, "pageSide": "right", "pageMeta": { "currentPageCount": 1, "pageMin": 24, "pageMax": 200, "pageIncrement": 1, "isValid": false } } } ``` **Response (200 OK — 같은 페이지 범위에 이어 배치)** ```json { "success": true, "message": "내지가 갱신되었습니다", "data": { "result": "updated", "breakBefore": "none", "pageNum": 2, "pageSide": "right", "pageMeta": { "currentPageCount": 2, "pageMin": 24, "pageMax": 200, "pageIncrement": 1, "isValid": false } } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 같은 페이지 범위 내 기존 콘텐츠에 이어 배치되어 저장된 경우 (응답 메시지 `Content updated successfully`, `result: "updated"`) | | 201 Created | — | 새 페이지에 콘텐츠 신규 추가 (응답 메시지 `Content created successfully`, `result: "inserted"`) | | 400 Bad Request | `ERR_VALIDATION_FAILED` / `ERR_CREATION_TYPE_UNSUPPORTED` | `templateUid` 누락, 필수 이미지 파라미터 누락, `parameters` JSON 파싱 실패, 잘못된 `breakBefore` 값, 이미 최종화된 책("이미 최종화된 책입니다. 콘텐츠를 추가할 수 없습니다."), 삭제된 책, `PDF_UPLOAD`/`MIX_COVER_TEMPLATE` 책 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 책 또는 템플릿 없음 | | 429 Too Many Requests | `ERR_TOO_MANY_REQUESTS` | 업로드 Rate Limit 초과 — [Rate Limiting 가이드](/docs/operations/rate-limiting) 참고 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 본 엔드포인트의 주요 검증 실패 메시지(`400 ERR_VALIDATION_FAILED`): - 이미 최종화된 책 — `errors[0]`: `"이미 최종화된 책입니다. 콘텐츠를 추가할 수 없습니다."` - 필수 이미지 파라미터 누락 — `errors[0]`: `"필수 이미지 파라미터 '' (<설명>)이 제공되지 않았습니다. form-data에 파일을 첨부하거나 parameters에 URL 또는 서버 파일명을 제공해야 합니다."` - 간지/발행면 템플릿에 잘못된 `breakBefore` — `errors[0]`: `"template_kind='divider'인 특수 페이지에서는 breakBefore='none'를 사용할 수 없습니다. 'page'만 허용됩니다."` 파라미터 이름과 설명은 템플릿에 따라 달라집니다. [Templates API](/docs/api/templates)에서 해당 템플릿의 필수 변수 목록을 먼저 확인하세요. 간지(`templateKind=divider`)·발행면(`templateKind=publish`) 템플릿은 `breakBefore`를 생략하거나 `page`만 명시해야 합니다. 일반 내지(`templateKind=content`)는 세 값 모두 허용됩니다. 응답 6필드 shape, 다른 errorCode, 처리 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ## 책 최종화 POST `/books/{bookUid}/finalization` 표지·콘텐츠(내지) 추가를 마친 뒤 이 엔드포인트를 호출해 책을 최종화합니다. 호출 시 책의 `status`가 `draft`에서 `finalized`로 전환되어 주문 가능 상태가 됩니다. > ⚠️ **사전 조건**: 콘텐츠(내지) 페이지가 1개 이상 추가되어 있어야 하며, 누적된 실 페이지 수가 판형의 페이지 수 규칙(`pageMin`·`pageMax`·`pageIncrement`)을 충족해야 합니다. > > > **전환 결과**: 호출 성공 시 `status`가 `draft` → `finalized`로 전환됩니다 (PDF 렌더링은 시스템이 비동기로 처리). 책의 `pdfStatus`는 PDF 렌더링이 완료되면 `2`(COMPLETED)로 갱신됩니다. ### Request 요청 본문은 비어 있습니다. 다만 IIS 환경 특성상 `Content-Length: 0` 헤더가 필요합니다 (없으면 411 Length Required). ### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/finalization' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Length: 0' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `result` | string | 처리 결과 메시지 (아래 **result 값 조건** 표 참고) | | `pageMeta` | object | 페이지 제약 메타. `currentPageCount`(최종화된 책의 내지 페이지 수, 콘텐츠 추가 결과로 자동 산출) + `pageMin`·`pageMax`·`pageIncrement`·`isValid` 필드 포함 | | `finalizedAt` | datetime | 최종화 완료 시각 (ISO 8601) | #### `result` 값 조건 | 값 | 발생 조건 | | --- | --- | | `"페이지를 추가하고 완료"` | 제본 방식 `PUR`이며 마지막 페이지가 자동 추가되어 finalize 완료된 경우 | | `"페이지를 추가하지 않고 완료"` | 제본 방식 `PUR`이며 마지막 페이지가 이미 있어 추가 없이 finalize 완료된 경우 | | `"완료"` | 제본 방식이 `PUR`이 아닌 경우 (일반 finalize 완료) | | `"updated"` | 이미 `finalized` 상태인 책에 finalize를 다시 호출한 경우 (멱등 응답). 실제 상태는 재변경되지 않음 | 파트너는 `result` 문자열 자체보다 응답 상태 코드(2xx)와 `pageMeta.currentPageCount`/`finalizedAt` 값으로 성공 여부를 확인하는 것을 권장합니다. ### Response 예시 **Response (201 Created — 신규 최종화)** ```json { "success": true, "message": "책 최종화 완료", "data": { "result": "페이지를 추가하고 완료", "pageMeta": { "currentPageCount": 26, "pageMin": 24, "pageMax": 200, "pageIncrement": 1, "isValid": true }, "finalizedAt": "2026-04-14T07:15:44.613Z" } } ``` **Response (200 OK — 이미 finalized 상태에서 재호출)** ```json { "success": true, "message": "이미 최종화된 책입니다", "data": { "result": "updated", "pageMeta": { "currentPageCount": 26, "pageMin": 24, "pageMax": 200, "pageIncrement": 1, "isValid": true }, "finalizedAt": "2026-04-14T07:15:44.613Z" } } ``` ### 전제조건별 에러코드 매핑 finalize 호출 시 전제조건을 충족하지 못하면 아래 표대로 `errorCode` + `fieldErrors`가 반환됩니다. 클라이언트는 `fieldErrors[].field`·`fieldErrors[].constraint`로 누락 항목을 식별해 사용자에게 안내하세요. | 전제조건 | `errorCode` | `fieldErrors[].field` | `constraint` | | --- | --- | --- | --- | | 책이 DRAFT 상태가 아님 | `ERR_FINALIZE_PREREQ_UNMET` | `status` | `enum` | | 내지 콘텐츠 미추가 (0건) | `ERR_FINALIZE_PREREQ_UNMET` | `contents` | `required` | | 페이지 수 < `pageMin` | `ERR_INSUFFICIENT_PAGES` | `pageCount` | `min` | | 페이지 수 > `pageMax` | `ERR_PAGECOUNT_INVALID` | `pageCount` | `max` | | 페이지 증분 규칙 위반 | `ERR_PAGECOUNT_INVALID` | `pageCount` | `increment` | 여러 전제조건이 동시에 깨지면 `fieldErrors`에 항목들이 함께 반환됩니다. 응답 예시는 위 "주요 에러 응답"과 [페이지 제약 위반 에러](#page-constraint-errors) 섹션을 참조하세요. > ⚠️ **`pageMeta.isValid` 사용 시 주의:** > > - `isValid`는 **페이지 수 제약**(`pageMin`·`pageMax`·`pageIncrement`)**만** 판정합니다. > - 내지 콘텐츠 추가 여부, 책 상태(DRAFT 여부) 등 다른 전제조건은 finalize 호출 시 별도로 검증되므로 `isValid: true`여도 `ERR_FINALIZE_PREREQ_UNMET`이 반환될 수 있습니다. > - **finalize 가능 여부 최종 확정은 `POST /finalization` 호출 결과로만 가능**합니다. `isValid`는 사전 UI 안내(예: "아직 N페이지 부족") 용도로만 사용하세요. ### HTTP 상태 코드 | 코드 | `errorCode` | 설명 | | --- | --- | --- | | 200 OK | — | 이미 `finalized` 상태의 책에 재호출한 경우 (멱등 응답, `message: "이미 최종화된 책입니다"`, `result: "updated"`) | | 201 Created | — | 최종화 신규 성공 | | 400 Bad Request | `ERR_FINALIZE_PREREQ_UNMET` / `ERR_INSUFFICIENT_PAGES` / `ERR_PAGECOUNT_INVALID` | 전제조건 미충족 (위 매핑 표 참고) | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 책 없음 | | 411 Length Required | — | `Content-Length: 0` 헤더 누락 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ### book.pdfStatus 값 책 조회(`GET /books`) 응답의 `pdfStatus` 필드는 PDF 처리 상태를 나타내는 정수값입니다. TEMPLATE 방식은 finalize 후 시스템이 PDF를 비동기 렌더링하므로, finalize 직후엔 `1`(PENDING) 상태에서 `2`(COMPLETED)로 전환됩니다. 파트너는 일반적으로 책의 `status` 필드 또는 주문 상태 흐름의 `PDF_READY` 전환을 기준으로 처리하면 충분합니다. | 값 | 의미 | 발생 시점 | | --- | --- | --- | | `null` | 미생성 (초기 상태) | 책 생성 직후 또는 콘텐츠만 추가한 상태 (finalize 미호출) | | `1` | PENDING (PDF 렌더링 대기) | finalize 호출 직후 (시스템이 PDF 렌더링을 시작한 상태) | | `2` | COMPLETED (PDF 준비 완료) | 시스템이 PDF 렌더링을 완료한 시점. 이 시점부터 주문 아이템이 `PDF_READY`(25)로 자동 승격됨 | | `9` | FAILED (처리 실패) | PDF 렌더링 실패 시 (드물게 발생). 발생 시 책을 다시 점검하거나 콘텐츠를 수정하여 재 finalize 시도 | 본 엔드포인트의 finalize 특화 에러: - **콘텐츠 미추가 상태에서 finalize 호출** — `400 ERR_FINALIZE_PREREQ_UNMET`, `fieldErrors[0].field: "contents"`·`constraint: "required"`. `POST /books/{bookUid}/contents`로 최소 1개 이상의 내지를 추가한 뒤 finalize를 호출하세요. - **페이지 수 규칙 위반** — 판형의 `pageMin`/`pageMax`/`pageIncrement` 검증. 응답 예시는 아래 [페이지 제약 위반 에러](#page-constraint-errors) 섹션을 참조하세요. 응답 6필드 shape, 다른 errorCode, 처리 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ## 이미지 처리 템플릿 표지·내지 추가 시 사용할 이미지를 어떻게 전달하고, 어떤 제약이 있는지 다룹니다. 본 묶음은 표지(`POST /cover`)·내지(`POST /contents`) 추가 단계의 사전 정보입니다. ### 이미지 전달 방식 (3가지, 혼용 가능) 표지·내지 추가 요청에서 이미지는 다음 3가지 방식 중 어느 것으로든 제공할 수 있고, 한 요청에서 혼용할 수도 있습니다. 템플릿의 이미지 필드명(`$변수명$`)과 요청 필드명이 일치해야 합니다. | 방식 | 전달 위치 | 적합한 상황 | | --- | --- | --- | | **① multipart 파일 직접 첨부** | `multipart/form-data`의 파일 필드 (예: `frontPhoto=@front.jpg`) | 한 페이지에 1~2장 이미지를 즉시 업로드할 때 | | **② URL 전달** | `parameters` JSON 안에 이미지 URL 문자열 (예: `"frontPhoto": "https://..."`) | 이미지가 외부 호스팅(또는 파트너 CDN)에 이미 있는 경우 | | **③ 사전 업로드한 서버 파일명 참조** | `parameters` JSON에 서버가 발급한 파일명 (예: `photo250105143052123.JPG`) | 여러 페이지에 같은 이미지를 반복 사용하거나, 다수 이미지를 미리 일괄 업로드해 두는 경우 (아래 **사진 사전 업로드** 섹션 사용) | ### 혼합 방식과 순서 제어 (`$upload` 플레이스홀더) 한 요청에서 위 3가지 방식을 섞어 쓸 수 있습니다. `parameters`의 배열 필드에 `$upload` 플레이스홀더를 넣으면 multipart로 첨부한 파일이 해당 위치에 차례로 들어갑니다. **parameters 예시 (혼합 방식)** ```json { "photos": [ "$upload", "https://example.com/photo2.jpg", "photo250105143052123.JPG", "$upload" ] } ``` 위 예시는 **첫 번째 업로드 파일 → URL 이미지 → 사전 업로드한 서버 파일 → 두 번째 업로드 파일** 순으로 배치됩니다. multipart로 첨부한 파일이 `$upload` 등장 순서대로 매핑됩니다. ### EXIF 정보 업로드된 이미지의 EXIF 정보는 **항상 보존**됩니다. EXIF 보존 여부를 제어하는 별도 파라미터는 제공하지 않습니다. ### 파일 업로드 제한 (이미지) 표지·내지 추가 요청의 multipart 파일 첨부, 사진 사전 업로드, URL 다운로드 모두에 동일한 이미지 제약이 적용됩니다. | 항목 | 제한 | | --- | --- | | 파일당 최대 크기 | 50MB | | 지원 포맷 | JPEG, PNG, GIF, WebP, BMP, HEIC, HEIF | | 업로드 Rate Limit | `upload` 정책 (API Key 단위 200 req/분) | | URL 다운로드 타임아웃 | 10초 (응답 없으면 400) | #### 파일 검증 - **매직 바이트 검증**: 파일 확장자뿐만 아니라 매직 바이트(파일 시그니처)를 확인하여 실제 이미지 파일인지 검증합니다. 확장자만 변경한 파일은 거부됩니다. - **자동 포맷 변환**: GIF·WebP는 PNG로, BMP·HEIC·HEIF는 JPG로 자동 변환되어 저장됩니다. - **EXIF 보존**: EXIF 정보는 항상 보존됩니다. Rate Limit 초과 시 429 응답과 `Retry-After` 헤더가 반환됩니다. 자세한 정책은 [Rate Limiting 가이드](/docs/operations/rate-limiting)를 참고하세요. ## 사진 사전 업로드 [선택] 이미지를 표지·내지 추가 요청에 직접 첨부하지 않고 미리 업로드해 서버 파일명을 받아두는 보조 API입니다. **표지/내지 추가 요청에 이미지를 직접 첨부(방식 ①)하거나 URL로 전달(방식 ②)한다면 이 단계는 생략 가능**합니다. 여러 페이지에 같은 이미지를 반복 사용하거나, 다수 이미지를 한 번에 모아 업로드해두고 싶을 때 활용하세요. ### 호출 도메인 기준 사진 관련 4개 엔드포인트는 모두 **API Key의 환경(Sandbox/Live)과 호출 도메인이 일치**해야 합니다. 잘못된 조합으로 호출 시 `403 ERR_ENV_MISMATCH`가 반환됩니다. | 환경 | 도메인 | 접근 가능 리소스 | | --- | --- | --- | | Sandbox | `api-sandbox.sweetbook.com` | 샌드박스(`isTest=true`) 책·사진 | | Live | `api.sweetbook.com` | 라이브(`isTest=false`) 책·사진 | ### 권한 매트릭스 파트너 관점에서 사진 4개 엔드포인트의 호출 가능 여부입니다. | 액션 | 본인 책 | 환경 불일치 | | --- | --- | --- | | 업로드 `POST /photos` | ✅ 201 | ❌ 403 `ERR_ENV_MISMATCH` | | 목록 `GET /photos` | ✅ 200 | ❌ 403 `ERR_ENV_MISMATCH` | | 다운로드 `GET /photos/{fileName}` | ✅ 200 | ❌ 403 `ERR_ENV_MISMATCH` | | 삭제 `DELETE /photos/{fileName}` | ✅ 204 | ❌ 403 `ERR_ENV_MISMATCH` | ### 사진 업로드 POST `/books/{bookUid}/photos` 한 번에 한 장씩 업로드하며, 응답으로 서버가 발급한 고유 파일명을 받습니다. 이후 표지·내지 추가 요청의 `parameters`에 이 파일명을 사용해 이미지를 참조합니다. #### Request — multipart/form-data | 필드 | 위치 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | --- | | `bookUid` | path | string | O | 대상 책 UID | | `file` | form-data | file | O | 업로드할 이미지 파일 (한 번에 1장) | #### 제약 사항 - **파일 크기**: 50MB 이하 (초과 시 400) - **책당 최대 200장**: 한 책에 사전 업로드할 수 있는 사진은 최대 200장입니다. 초과 전에 [사진 목록 조회](#photos-list)로 현재 수량을 확인하세요 - **책 상태 제약**: `draft` 상태의 책에만 업로드 가능합니다. `finalized`(최종화) 또는 `deleted`(삭제) 상태의 책에는 업로드할 수 없습니다 — 최종화 전에 필요한 사진을 모두 업로드하세요 - **지원 포맷**: JPEG · PNG · GIF · WebP · BMP · HEIC · HEIF — 그 외 형식은 400 거부. 매직 바이트로 실제 형식을 검증하므로 확장자만 바꾼 파일은 거부됨 #### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/photos' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'file=@photo1.jpg' ``` #### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `fileName` | string | 서버가 발급한 고유 파일명. 표지·내지 추가 요청의 `parameters`에 이 값을 사용해 이미지를 참조하거나, [사진 다운로드](#photos-download) URL 경로에 직접 사용 | | `originalName` | string | 업로드한 원본 파일명 | | `size` | integer | 저장된 파일 크기 (bytes) | | `mimeType` | string | MIME 타입 (예: `image/jpeg`, `image/png`) | | `uploadedAt` | datetime | 업로드 완료 시각 (ISO 8601) | | `isDuplicate` | boolean | 같은 책에 동일 파일(해시 기준)이 이미 업로드되어 있던 경우 `true`. 이때 `fileName`은 기존 파일의 파일명을 반환 | | `hash` | string | 파일 내용의 MD5 해시값 (중복 검사에 사용됨) | #### Response 예시 **Response (201 Created)** ```json { "success": true, "message": "Photo uploaded successfully", "data": { "fileName": "photo260423001516528.JPG", "originalName": "cover.jpg", "size": 382229, "mimeType": "image/jpeg", "uploadedAt": "2026-04-23T00:15:19.136Z", "isDuplicate": false, "hash": "1baf5077ae9f5a13787a73a08625a802" } } ``` #### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 201 Created | — | 업로드 성공 (응답에 서버 파일명 포함) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | 파일 누락, 지원되지 않는 파일 형식, 50MB 초과 등 | | 400 Bad Request | `ERR_CREATION_TYPE_UNSUPPORTED` | `PDF_UPLOAD` 타입 책에 사진 업로드 시도 — 사진 업로드는 `TEMPLATE` 또는 `MIX_COVER_TEMPLATE` 방식에서만 지원 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 403 Forbidden | `ERR_FORBIDDEN` | 본인 소유가 아닌 책 | | 403 Forbidden | `ERR_ENV_MISMATCH` | 호출 도메인과 API Key의 환경(Sandbox/Live) 불일치 — [호출 도메인 기준](#photos-domain) 참고 | | 404 Not Found | `ERR_NOT_FOUND` | 책 없음 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 본 엔드포인트의 주요 검증 실패 메시지: - `400 ERR_VALIDATION_FAILED` 지원되지 않는 파일 형식 — `errors[0]`: `"지원되지 않는 파일 형식입니다: <파일명>. (확장자: <확장자>)"` - `400 ERR_VALIDATION_FAILED` 파일 크기 초과 — `errors[0]`: `"파일 크기가 50MB를 초과합니다"` - `400 ERR_VALIDATION_FAILED` 파일 누락 — `errors[0]`: `"파일이 없습니다"` - `400 ERR_CREATION_TYPE_UNSUPPORTED` PDF_UPLOAD 타입 책에 사진 업로드 시도 — `errors[0]`: `"PDF_UPLOAD 타입 책에서는 지원하지 않는 API입니다."` - `403 ERR_ENV_MISMATCH` 환경 불일치 — `errors[0]`: `"환경 불일치: 호출 도메인과 API Key의 환경(sandbox/live)이 일치하지 않습니다. 키에 맞는 도메인으로 재시도하세요."` 응답 6필드 shape, 다른 errorCode, 처리 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ### 사진 목록 조회 GET `/books/{bookUid}/photos` 해당 책에 사전 업로드한 사진 파일명 목록을 조회합니다. #### Request 예시 **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/photos' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` #### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `data` | array | 업로드된 사진 항목 배열 (아래 항목 필드 참고). list envelope 평탄화 적용 (이전 `data.photos` → `data`) | | `pagination.total` | integer | 업로드된 사진 총 개수 (이전 `data.totalCount` → `pagination.total`로 이전) | #### 사진 항목 필드 (`data[]`) | 필드 | 타입 | 설명 | | --- | --- | --- | | `fileName` | string | 서버가 발급한 고유 파일명. 표지·내지 추가 요청의 `parameters` 또는 [사진 다운로드](#photos-download) URL에 사용 | | `originalName` | string | 원본 파일명 | | `size` | integer | 파일 크기 (bytes) | | `mimeType` | string | MIME 타입 | | `uploadedAt` | datetime | 업로드 시각 (ISO 8601) | | `hash` | string | 파일 내용 MD5 해시값 | #### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "Success", "data": [ { "fileName": "photo260423001516528.JPG", "originalName": "cover.jpg", "size": 382229, "mimeType": "image/jpeg", "uploadedAt": "2026-04-23T00:15:18.977Z", "hash": "1baf5077ae9f5a13787a73a08625a802" } ], "pagination": { "total": 1, "limit": 100, "offset": 0, "hasNext": false } } ``` #### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 (사진이 없으면 `data` 빈 배열 + `pagination.total=0`) | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 403 Forbidden | `ERR_FORBIDDEN` | 본인 소유가 아닌 책 | | 403 Forbidden | `ERR_ENV_MISMATCH` | 호출 도메인과 API Key의 환경(Sandbox/Live) 불일치 — [호출 도메인 기준](#photos-domain) 참고 | | 404 Not Found | `ERR_NOT_FOUND` | 책 없음 | ### 사진 삭제 DELETE `/books/{bookUid}/photos/{fileName}` 사전 업로드한 사진을 삭제합니다. 응답 본문 없이 **204 No Content**를 반환합니다. #### Path 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | O | 대상 책 UID | | `fileName` | string | O | 삭제할 사진의 서버 파일명 (업로드 응답의 `fileName`) | #### Request 예시 **Request** ```bash curl -X DELETE 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/photos/photo250105143052123.JPG' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` #### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 204 No Content | — | 삭제 성공 (응답 본문 없음) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `bookUid` 또는 `fileName` 형식 오류 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 403 Forbidden | `ERR_FORBIDDEN` | 본인 소유가 아닌 책 | | 403 Forbidden | `ERR_ENV_MISMATCH` | 호출 도메인과 API Key의 환경(Sandbox/Live) 불일치 — [호출 도메인 기준](#photos-domain) 참고 | | 404 Not Found | `ERR_NOT_FOUND` | 책 또는 파일 없음 | | 409 Conflict | `ERR_CONFLICT` | 책이 이미 최종화(`finalized`)된 상태 — 최종화 이후에는 사진 삭제가 차단됩니다 | ### 사진 다운로드 GET `/books/{bookUid}/photos/{fileName}` 업로드한 사진의 **800px 썸네일 이미지 바이트**를 반환합니다. 긴 변 기준으로 800px로 리사이즈된 썸네일이며, 원본(4000px)은 본 API로 제공되지 않습니다(내부 PDF 렌더링·인쇄 용도로만 사용). 업로드 정상 처리 여부 확인이나 사진 미리보기 UI 구성 용도로 사용하세요. > ⚠️ **브라우저 ``에 직접 사용 불가**합니다. 인증 헤더를 붙일 수 없기 때문입니다. 파트너 백엔드가 받아 자체 시스템에 저장·프록시해 노출하는 흐름을 권장합니다. #### Path 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | O | 대상 책 UID | | `fileName` | string | O | 다운로드할 사진의 서버 파일명 | #### Request 예시 **Request (파일로 저장)** ```bash curl 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/photos/photo260424005709732.PNG' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ --output photo.png ``` #### 응답 헤더 | 헤더 | 값 예시 | 설명 | | --- | --- | --- | | `Content-Type` | `image/jpeg` · `image/png` · `image/webp` 등 | 저장된 원본의 MIME 타입 | | `Content-Length` | 58217 | 썸네일 바이트 크기 | #### 응답 본문 800px 썸네일 이미지 바이트 (바이너리). `curl --output`이나 파일 스트림으로 저장해 사용하세요. #### Response 예시 **Response (200 OK)** ```http HTTP/1.1 200 OK Content-Type: image/png Content-Length: 58217 ``` #### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 다운로드 성공 (`Content-Type: image/*` + 이미지 바이트) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `bookUid` 또는 `fileName` 형식 오류 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 403 Forbidden | `ERR_FORBIDDEN` | 본인 소유가 아닌 책 | | 403 Forbidden | `ERR_ENV_MISMATCH` | 호출 도메인과 API Key의 환경(Sandbox/Live) 불일치 — [호출 도메인 기준](#photos-domain) 참고 | | 404 Not Found | `ERR_NOT_FOUND` | 책 또는 파일 없음 (`errors: ["사진을 찾을 수 없습니다"]` — 예: 삭제된 파일명) | **404 ERR_NOT_FOUND**: 파일 없음 (삭제된 `fileName` 등) — `errors[0]`: `"사진을 찾을 수 없습니다"`. 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_NOT_FOUND](/docs/operations/errors#ERR_NOT_FOUND)를 참조하세요. ## PDF 다운로드 책이 최종화되어 시스템이 표지·내지를 PDF로 렌더링한 뒤에는 다음 두 엔드포인트로 인쇄용 PDF를 다운로드할 수 있습니다. - `GET /books/{bookUid}/pdf-cover` — 표지 PDF - `GET /books/{bookUid}/pdf-contents` — 내지 PDF > ℹ️ TEMPLATE 책의 표지·내지 PDF는 모두 **책 최종화 후 백엔드가 백그라운드 큐에서 렌더링**합니다(그룹 B). 따라서 `POST /finalization` 호출 직후에는 PDF가 아직 준비되지 않았을 수 있으며, 응답이 `pdfStatus`에 따라 분기됩니다 — 일정 시간 후 재시도해야 정상 다운로드가 가능합니다. 정상 응답은 JSON이 아닌 `application/pdf` 바이너리 스트림입니다. 그 외 케이스는 6필드 JSON 에러 응답으로 errorCode·errors·fieldErrors가 포함됩니다. **Request — 표지 PDF 다운로드** ```bash curl -o cover.pdf \\ 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Request — 내지 PDF 다운로드** ```bash curl -o contents.pdf \\ 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/pdf-contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 매트릭스 (그룹 B) TEMPLATE 책의 PDF 다운로드는 책 상태(`pdfStatus`)에 따라 다음과 같이 분기됩니다. 클라이언트는 `errorCode`로 분기하며 `errors[0]`의 한글 메시지를 사용자에게 보여줄 수 있습니다. | 책 상태 | HTTP | 응답 형식 | errorCode | 클라이언트 권장 행동 | | --- | --- | --- | --- | --- | | 책 최종화 전 (PDF 생성 미시작) | 409 | JSON | `ERR_PDF_NOT_GENERATED` | `POST /books/{bookUid}/finalization` 호출 후 재시도 | | PDF 생성 진행 중 | 409 | JSON | `ERR_PDF_PENDING` | 일정 간격(수 초~수십 초) 후 재시도. 무한 폴링 금지. `Retry-After` 헤더는 본 응답에 포함되지 않으므로 클라이언트가 자체 대기 간격을 결정해야 합니다. | | PDF 생성 실패 | 422 | JSON | `ERR_PDF_GENERATION_FAILED` | 클라이언트 자체 해결 불가 — 사용자 안내 + 고객지원팀 문의 | | 생성 완료 + 파일 부재 (서버 결함) | 500 | JSON | `ERR_PDF_FILE_MISSING` | 서버 측 결함 — 고객지원팀 문의 | | 생성 완료 + 파일 존재 | 200 | `application/pdf` 바이너리 스트림 | — | 응답 본문을 PDF 파일로 저장 | #### 공통 사전 검증 | 케이스 | HTTP | errorCode | | --- | --- | --- | | 책 미존재 | 404 | `ERR_NOT_FOUND` | | 본인 소유가 아닌 책 | 403 | `ERR_FORBIDDEN` | | 환경 불일치 (Sandbox/Live 도메인 ↔ 책의 환경 불일치) | 403 | `ERR_ENV_MISMATCH` | | kind 경로 오류 (`/pdf-cover`·`/pdf-contents` 외 호출) | 400 | `ERR_MALFORMED_REQUEST` | | 책의 `creationType`이 PDF 조회를 지원하지 않음 | 400 | `ERR_CREATION_TYPE_UNSUPPORTED` | **그룹 B 분기 응답 예시**: [409 ERR_PDF_NOT_GENERATED](/docs/operations/errors#ERR_PDF_NOT_GENERATED) (최종화 전) · [409 ERR_PDF_PENDING](/docs/operations/errors#ERR_PDF_PENDING) (생성 진행 중) · [422 ERR_PDF_GENERATION_FAILED](/docs/operations/errors#ERR_PDF_GENERATION_FAILED) (생성 실패) · [500 ERR_PDF_FILE_MISSING](/docs/operations/errors#ERR_PDF_FILE_MISSING) (서버 결함). 처리 가이드 포함 종합 안내는 [에러 코드 & 트러블슈팅 — PDF 다운로드 분기](/docs/operations/errors#pdf-download-branching)를 참조하세요. ## 책 목록 조회 GET `/books` 파트너가 생성한 책 목록을 조회합니다. 다양한 쿼리 파라미터로 필터링할 수 있으며, 페이지네이션을 지원합니다. 응답에는 PDF 방식·템플릿 방식 책이 모두 포함되며, `creationType` 필드로 구분할 수 있습니다. ### Query 파라미터 | 파라미터 | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | — | 특정 책 UID로 단건 조회 | | `status` | string | — | 책 상태 필터. `draft` / `finalized` / `deleted`(일반 파트너 조회 결과에서는 삭제된 책이 자동 제외되므로 빈 결과) | | `pdfStatus` | string | — | PDF 상태 필터. 숫자 값(`1`/`2`/`9`) 또는 `null` 문자열(PDF 미생성 책) 허용. TEMPLATE 방식은 finalize 후 PDF 렌더링 진행 중인 책(`1`)과 완료된 책(`2`)을 구분할 때 활용 | | `specProfileUid` | string | — | SpecProfile UID로 필터 | | `createdFrom` | string | — | 생성일 시작 (ISO 8601, 예: `2026-01-01`) | | `createdTo` | string | — | 생성일 종료 (ISO 8601) | | `limit` | integer | 20 | 조회할 항목 수 (1~100) | | `offset` | integer | 0 | 건너뛸 항목 수 (≥0) | 페이지네이션 동작과 응답 구조(`data.books` 배열 + `data.pagination` 객체)는 [페이지네이션 가이드](/docs/operations/pagination)를 참고하세요. ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 책 고유 UID | | `accountUid` | string | 책 소유 계정 UID | | `title` | string | 책 제목 | | `author` | string | null | 저자명 (선택) | | `status` | string | 책 상태 — enum 문자열로 반환. `"draft"`(작성 중, 내부값 1) / `"finalized"`(최종화 완료, 내부값 2) / `"deleted"`(삭제됨, 내부값 9) | | `pageCount` | integer | 내지 페이지 수. TEMPLATE은 콘텐츠 추가 결과로 자동 산출 (finalize 시점에 확정), PDF 방식은 책 생성 시 지정한 값 | | `bookSpecUid` | string | 판형 UID | | `specProfileUid` | string | null | SpecProfile UID | | `creationType` | string | `TEMPLATE` / `PDF_UPLOAD` / `MIX_COVER_TEMPLATE` | | `createdAt` | datetime | 책 생성 시각 (ISO 8601) | | `updatedAt` | datetime | 마지막 수정 시각 | | `externalRef` | string | null | 파트너 외부 참조 식별자 | | `isTest` | boolean | Sandbox 환경 책 여부 | | `pdfStatus` | integer | null | PDF 처리 상태 (정수). `null`(미생성) / `1`(PENDING, 렌더링 대기) / `2`(COMPLETED, 준비 완료) / `9`(FAILED, 실패). 자세한 의미는 위 **책 최종화 → book.pdfStatus 값** 표 참고. **TEMPLATE 방식은 finalize 직후 1을 거쳐 2로 전환**됩니다 (PDF 방식은 즉시 2) | | `pdfCreatedAt` | datetime | null | PDF 파일이 마지막으로 갱신된 시각 | | `pdfRequestedAt` | datetime | null | 시스템이 PDF 렌더링 큐에 요청을 등록한 시각. TEMPLATE 방식은 finalize 호출 직후 시스템이 내부적으로 갱신하며, PDF 방식은 일반적으로 `null`을 유지합니다. 파트너가 직접 제어하는 필드가 아닙니다 | | `thumbnailStatus` | integer | null | 책 페이지 썸네일 처리 상태. `null`(NONE, 미생성) / `1`(PENDING, 생성 진행 중) / `2`(COMPLETED, 생성 완료) / `9`(FAILED, 생성 실패) | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "성공", "data": [ { "bookUid": "bk_2dyG********", "accountUid": "u_b4b9********************", "title": "템플릿 방식 책", "author": null, "status": "draft", "pageCount": 0, "bookSpecUid": "SQUAREBOOK_HC", "specProfileUid": null, "creationType": "TEMPLATE", "createdAt": "2026-04-22T05:06:05.989Z", "updatedAt": "2026-04-22T05:06:05.989Z", "externalRef": null, "isTest": true, "pdfStatus": null, "pdfCreatedAt": null, "pdfRequestedAt": null, "thumbnailStatus": null }, { "bookUid": "bk_2h4L********", "title": "PDF 업로드 샘플", "status": "finalized", "creationType": "PDF_UPLOAD", "pageCount": 24, "pdfStatus": 2, "pdfCreatedAt": "2026-04-22T07:46:24.000Z" /* ... 나머지 필드 생략 ... */ } ], "pagination": { "total": 15, "limit": 5, "offset": 0, "hasNext": true } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `limit`/`offset` 범위 위반 등 파라미터 오류 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## 책 단건 조회 GET `/books/{bookUid}` 특정 책의 상세 정보를 조회합니다. v1.2에 신설된 RESTful 단건 조회 엔드포인트로, 편집 화면 진입이나 현재 페이지 수 제약 확인에 사용합니다. 응답에 `pageMeta`가 포함되어 "아직 N페이지 부족" 같은 힌트를 UI에 즉시 반영할 수 있습니다. ### Path 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | O | 조회할 책 UID | ### Request 예시 **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 책 UID | | `accountUid` | string | 책을 생성한 계정의 UID (파트너 본인 계정) | | `title` | string | 책 제목 | | `bookSpecUid` | string | 책 규격 UID | | `bookSpecName` | string | 규격 이름 (예: "A5 소프트커버 포토북") | | `specProfileUid` | string | null | SpecProfile UID (사용 시) | | `creationType` | string | `TEMPLATE` / `PDF_UPLOAD` / `MIX_COVER_TEMPLATE` | | `status` | integer | `1`=작성 중(DRAFT) / `2`=최종화 완료(FINALIZED). `status`가 `1`인 책은 주문을 생성할 수 없으며, `POST /books/{bookUid}/finalization`을 호출해 `2`로 전이해야 합니다. **목록 조회(`GET /books`)는 동일 값을 문자열**(`"draft"` / `"finalized"`)**로 반환**합니다 | | `coverTemplateUid` | string | null | 템플릿 방식에서 바인딩된 표지 템플릿 UID. 표지 추가 전이나 PDF 방식은 `null` | | `externalRef` | string | null | 파트너 외부 참조 식별자 | | `isTest` | boolean | Sandbox 환경의 책이면 `true` | | `pageMeta` | object | 페이지 제약 메타. `currentPageCount`·`pageMin`·`pageMax`·`pageIncrement`·`isValid` 5개 필드 | | `createdAt` | datetime | 생성 시각 (ISO 8601) | | `updatedAt` | datetime | 최종 수정 시각 (ISO 8601) | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "성공", "data": { "bookUid": "bk_3k2S********", "accountUid": "u_b4b9********************", "title": "일기장 샘플", "bookSpecUid": "SQUAREBOOK_HC", "bookSpecName": "고화질 스퀘어북 (하드커버)", "specProfileUid": null, "creationType": "TEMPLATE", "status": 1, "coverTemplateUid": "tpl_cover_abc", "externalRef": "PARTNER-ORDER-001", "isTest": true, "pageMeta": { "currentPageCount": 18, "pageMin": 24, "pageMax": 200, "pageIncrement": 1, "isValid": false }, "createdAt": "2026-04-22T03:00:00.000Z", "updatedAt": "2026-04-22T04:00:00.000Z" } } ``` ### HTTP 상태 코드 | 상태 코드 | `errorCode` | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 403 Forbidden | `ERR_FORBIDDEN` | 다른 파트너의 책 접근 시도 | | 403 Forbidden | `ERR_ENV_MISMATCH` | Sandbox 도메인에서 Live 책 조회(또는 반대) | | 404 Not Found | `ERR_NOT_FOUND` | 책 UID가 존재하지 않음 | ### 단건 조회 vs 목록 조회 (`bookUid` 필터) 차이 기존 목록 조회의 `bookUid` 필터(`GET /books?bookUid=...`)와 본 단건 조회는 의미·응답 타입·권한 처리가 다릅니다. 용도에 맞게 선택하세요. | 항목 | `GET /books/{uid}` (단건) | `GET /books?bookUid=...` (목록 필터) | | --- | --- | --- | | 의미론 | RESTful 리소스 단건 조회 | 목록 조회 + `bookUid` 동등 필터 (결과 0~1건) | | 응답 `data` 타입 | 객체 | 배열 + `pagination` 메타 | | 응답 필드 | 상세 필드 + `pageMeta`, `bookSpecName`, `coverTemplateUid` 등 편집 화면용 풀셋 | 목록용 요약 필드 (`pageCount` 유지, `pageMeta` 없음) | | 결과 없을 때 | 404 `ERR_NOT_FOUND` | 200 + 빈 배열 | | 다른 파트너의 책 | 403 `ERR_FORBIDDEN` | 200 + 빈 배열 (필터 결과) | | 환경 불일치 (sandbox/live) | 403 `ERR_ENV_MISMATCH` | 빈 배열 (필터에 환경 적용) | | `status` 필드 타입 | 숫자 (`1`/`2`) | 문자열 (`"draft"`/`"finalized"`) | | 주 용도 | 책 편집 페이지 진입, 상세 패널 렌더링, finalize 전 상태 확인 | 목록/검색 UI, 페이지네이션, 여러 조건 조합 필터링 | - 책 UID를 알고 있고 **상세 정보(특히 `pageMeta`)가 필요**하면 → 단건 조회 - 존재 여부만 빠르게 확인하거나 여러 조건과 조합해야 하면 → 목록 조회 - **없는 책과 권한 없는 책을 명확히 구별해야 하면** → 단건 조회 (목록은 둘 다 빈 배열로 동일) **404 ERR_NOT_FOUND**: 존재하지 않는 `bookUid` — `errors[0]`: `"책을 찾을 수 없습니다: bk_xxx"`. 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_NOT_FOUND](/docs/operations/errors#ERR_NOT_FOUND)를 참조하세요. ## 페이지 제약 위반 에러 콘텐츠(내지) 추가·최종화 시 페이지 제약을 위반하면 아래 3종 에러 중 하나가 반환됩니다. `fieldErrors[0].constraint`로 어떤 제약이 위반됐는지 분기할 수 있습니다. 전체 에러 체계 상세는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참고하세요. - `ERR_INSUFFICIENT_PAGES` (`constraint: "min"`) — finalize 시점에 최소 페이지 미달 - `ERR_PAGECOUNT_INVALID` (`constraint: "max"`) — 콘텐츠 추가·finalize 시점에 최대 페이지 초과 - `ERR_PAGECOUNT_INVALID` (`constraint: "increment"`) — 증분 규칙 위반. `requiredValue`는 `{min, increment}` 객체 형태 응답 예시(`fieldErrors` 배열에 `currentValue`·`requiredValue`·`constraint` 포함)는 errors 페이지의 각 errorCode 카드를 참조하세요: [ERR_INSUFFICIENT_PAGES](/docs/operations/errors#ERR_INSUFFICIENT_PAGES), [ERR_PAGECOUNT_INVALID](/docs/operations/errors#ERR_PAGECOUNT_INVALID). ## 책 삭제 DELETE `/books/{bookUid}` 책을 삭제 처리합니다. **물리적 삭제가 아닌 소프트 삭제**(상태값 변경)로 처리되어, 이후 목록 조회 결과에서 제외됩니다. 표지·콘텐츠 페이지·사전 업로드한 사진은 별도 정리되지 않고 보관 상태로 남으며, 책 자체에 대한 추가 작업(표지·콘텐츠 추가/수정/finalize/주문)은 모두 차단됩니다. > ℹ️ **멱등 동작**: 이미 삭제된 책에 다시 호출해도 200 OK를 반환합니다 (재실패하지 않음). > > > **상태 제약 없음**: `draft`·`finalized` 어느 상태에서도 삭제 가능합니다 (단 본인 소유 책에 한함). ### Path 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `bookUid` | string | O | 삭제할 책 UID | ### Request 예시 **Request** ```bash curl -X DELETE 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookUid` | string | 삭제된 책 UID | | `status` | string | 삭제 처리 결과 (`"deleted"`) | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "책이 삭제되었습니다", "data": { "bookUid": "bk_3k2S********", "status": "deleted" } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 삭제 성공 (이미 삭제된 책에 대한 재호출 포함) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `bookUid` 형식 오류 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 403 Forbidden | `ERR_FORBIDDEN` | 본인 소유가 아닌 책 | | 404 Not Found | `ERR_NOT_FOUND` | 책 없음 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | **404 ERR_NOT_FOUND**: 존재하지 않는 `bookUid` — 응답 shape 상세는 [에러 코드 & 트러블슈팅 — ERR_NOT_FOUND](/docs/operations/errors#ERR_NOT_FOUND)를 참조하세요. ## 관련 문서 - [템플릿 기반 시나리오 — — 책 생성부터 주문까지 단계별 흐름](/docs/guides/scenario-template) - [혼합 방식 시나리오 — — 표지=템플릿, 내지=PDF 흐름](/docs/guides/scenario-mix) - [BookSpecs API — — 판형 정보 (페이지 수 규칙 포함) 조회](/docs/api/book-specs) - [Templates API — — 표지·내지 템플릿 목록 및 변수 정보 조회](/docs/api/templates) - [템플릿 구조와 바인딩 — — 변수, 파라미터, 이미지 매칭 동작 원리](/docs/concepts/template-structure) - [레이아웃 동작 원리 — — breakBefore, 페이지 흐름](/docs/concepts/layout-principles) - [갤러리 템플릿 — — 다수 이미지 배치 (collageGallery, rowGallery)](/docs/concepts/gallery) - [베이스 레이어 — — 표지·내지 공통 배경 레이어](/docs/concepts/base-layer) --- # BookSpecs API 포토북 판형의 종류와 특징을 안내하고, 목적에 맞는 판형을 선택하는 방법을 설명합니다. ## 판형(BookSpec)이란? 판형은 포토북의 물리적 제작 사양을 정의합니다. 판형 크기, 제본 방식, 표지 유형, 페이지 범위, 가격 등의 정보를 포함합니다. 책을 생성할 때 `bookSpecUid`를 지정하며, 이후 사용할 수 있는 템플릿과 페이지 규칙이 이 스펙에 의해 결정됩니다. 각 판형의 외관·재질·제본 등 자세한 판형 정보는 [판형 정보 ↗](https://api.sweetbook.com/partner/templates/pricing)에서 확인할 수 있습니다. ## 판형 종류 현재 제공되는 주요 판형입니다. `GET /book-specs`로 전체 목록을 조회할 수 있습니다. ### A4 소프트커버 포토북 `PHOTOBOOK_A4_SC` A4 크기의 소프트커버 포토북입니다. 글과 사진을 넉넉하게 배치할 수 있어 일기장, 알림장 등에 적합합니다. ### A5 소프트커버 포토북 `PHOTOBOOK_A5_SC` A5 크기의 소프트커버 포토북입니다. 최대 200페이지까지 지원하여 많은 사진을 담을 수 있습니다. ### 고화질 스퀘어북 (하드커버) `SQUAREBOOK_HC` 하드커버의 정사각 판형입니다. 고급스러운 마감으로 선물용, 졸업앨범 등에 적합합니다. ### 스퀘어북 레이플랫 (하드커버) `SQUAREBOOK_LAYFLAT_HC` 레이플랫 제본을 적용한 정사각 하드커버 판형입니다. PDF 업로드 시 내지는 펼침면 형식이며 업로드할 PDF 페이지수는 책 페이지수의 절반(`book.pageCount / 2`)입니다 — 자세한 내용은 [PDF 표지/내지 사이즈](/docs/concepts/pdf-size) 참조. ## 판형 조회하기 ### 목록 조회 사용 가능한 모든 판형 목록을 조회합니다. 각 상품의 UID, 이름, 바인딩 타입, 페이지 규칙 등이 반환됩니다. > ℹ️ **accountUid 자동 적용:** `accountUid` 파라미터를 전달하지 않으면 인증된 본인의 UID가 자동으로 적용됩니다. > 이를 통해 해당 계정에 설정된 커스텀 가격이 자동 반영됩니다. > 타인의 `accountUid`를 전달하면 **403 Forbidden**이 반환됩니다. **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/book-specs' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 상세 조회 bookSpecUid로 특정 판형의 상세 정보를 조회합니다. 가격 정보, 페이지 제한(최소/최대/단위), 크기 정보가 포함됩니다. > ℹ️ **accountUid 자동 적용:** `accountUid` 파라미터를 전달하지 않으면 인증된 본인의 UID가 자동으로 적용됩니다. > 이를 통해 해당 계정에 설정된 커스텀 가격이 자동 반영됩니다. > 타인의 `accountUid`를 전달하면 **403 Forbidden**이 반환됩니다. **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/book-specs/SQUAREBOOK_HC' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 주요 응답 필드 | 필드 | 설명 | | --- | --- | | `bookSpecUid` | 판형 고유 식별자 (책 생성 시 사용) | | `name` | 판형명 | | `innerTrimWidthMm` / `innerTrimHeightMm` | 내지 크기 (mm) | | `pageMin` / `pageMax` | 페이지 범위 | | `pageIncrement` | 페이지 증가 단위 | | `coverType` | 표지 유형 (Softcover, Hardcover) | | `bindingType` | 제본 유형 (PUR) | | `priceBase` | 기본 가격 | | `pricePerIncrement` | 페이지 증가당 추가 가격 | ## 가격 계산 제작 가격은 기본 가격에 추가 페이지 비용을 더해 산출됩니다. > ⚠️ 실제 주문 가격은 `POST /orders/estimate`로 확인하세요. 수량, 포장비, 배송비 등 추가 요소가 반영될 수 있습니다. **Price formula** ```text 총 가격 = priceBase + ((pageCount - pageMin) ÷ pageIncrement) × pricePerIncrement 예시: SQUAREBOOK_HC, 40페이지 기본 가격: 19,800원 (24페이지 포함) 추가 페이지: (40 - 24) = 16페이지 추가 비용: (16 ÷ 2) × 500 = 4,000원 총 가격: 19,800 + 4,000 = 23,800원 ``` ## 페이지 규칙 책을 최종화(`POST /books/{bookUid}/finalization`)할 때 페이지 수가 스펙의 규칙을 충족해야 합니다. - 페이지 수는 `pageMin` 이상, `pageMax` 이하여야 합니다. - 페이지 수는 `pageIncrement` 단위로 증가해야 합니다 (예: 24, 26, 28, ...). - 규칙에 맞지 않으면 최종화가 거부됩니다. ## 판형 선택 가이드 | 용도 | 추천 판형 | 이유 | | --- | --- | --- | | 일기장 / 알림장 | SQUAREBOOK_HC | 최대 130페이지로 한 달치 일기를 충분히 담을 수 있음 | | 문서 / 포트폴리오 | PHOTOBOOK_A4_SC | A4 규격으로 문서, 작품집 등에 적합 | | 대량 사진 앨범 | PHOTOBOOK_A5_SC | 최대 200페이지로 많은 사진을 담을 수 있음 | | 고급 포토북 / 선물용 | SQUAREBOOK_HC | 하드커버로 고급스러운 마감 | | 졸업앨범 / 범용 | SQUAREBOOK_HC | 정사각 판형으로 다양한 레이아웃 활용 가능 | | 레이플랫 제본 (펼침면 PDF) | SQUAREBOOK_LAYFLAT_HC | 레이플랫 제본 + 16~46페이지. 펼침면 PDF 형식 업로드 | ## 관련 문서 - [Books — PDF 기반 — — PDF 방식 책 생성 시 bookSpecUid 사용](/docs/api/books-pdf) - [Books — 템플릿 기반 — — 템플릿 방식 책 생성 시 bookSpecUid 사용](/docs/api/books-template) - [Templates API — — 판형에 맞는 템플릿 선택](/docs/api/templates) - [Orders API — — 가격 견적 및 주문](/docs/api/orders) --- # Templates API 템플릿을 조회하고 책에 적용하는 방법을 안내합니다. ## 템플릿이란? 템플릿은 포토북 페이지의 레이아웃을 정의하는 디자인 틀입니다. 텍스트, 사진, 그래픽 요소의 배치와 스타일이 미리 설정되어 있어, 파라미터만 전달하면 완성된 페이지를 만들 수 있습니다. 각 템플릿의 디자인 특징과 사용 예시는 [템플릿 상세 정보 ↗](https://api.sweetbook.com/partner/templates)에서 확인할 수 있습니다. ## 템플릿 종류 템플릿은 `templateKind`로 구분되며, 네 가지 종류가 있습니다. 현재 제공되는 모든 템플릿 카탈로그는 [템플릿 카탈로그 ↗](https://api.sweetbook.com/templates/)에서 미리 볼 수 있습니다. | 종류 | 값 | 용도 | 적용 API | | --- | --- | --- | --- | | 표지 템플릿 | `cover` | 책 표지 디자인. 책당 하나만 적용됩니다. | `POST /books/{bookUid}/cover` | | 내지 템플릿 | `content` | 내지 페이지 디자인. 여러 번 적용하여 페이지를 추가합니다. | `POST /books/{bookUid}/contents` | | 구분지 템플릿 | `divider` | 섹션 구분용 페이지 디자인. | `POST /books/{bookUid}/contents` | | 출판 템플릿 | `publish` | 출판용 특수 페이지 디자인. | `POST /books/{bookUid}/contents` | > ℹ️ 표지 템플릿과 내지 템플릿은 서로 다른 API로 적용합니다. 표지에 내지 템플릿을 사용하거나, 내지에 표지 템플릿을 사용할 수 없습니다. ## 카테고리 템플릿은 용도별 카테고리로 분류됩니다. `GET /template-categories`로 전체 목록을 조회할 수 있습니다. | 코드 | key | 한국어 | English | | --- | --- | --- | --- | | 1 | `diary` | 일기장 | Diary | | 2 | `notice` | 알림장 | Notice | | 3 | `album` | 앨범 | Album | | 4 | `yearbook` | 졸업앨범 | Yearbook | | 5 | `wedding` | 웨딩 | Wedding | | 6 | `baby` | 육아 | Baby | | 7 | `travel` | 여행 | Travel | | 8 | `etc` | 기타 | Etc | ## 템플릿 조회하기 ### 목록 조회 `GET /templates`로 사용 가능한 템플릿을 조회합니다. 공용(public) 템플릿과 본인이 생성한 템플릿이 함께 조회됩니다. | Query Parameter | 설명 | | --- | --- | | `bookSpecUid` | 판형 UID로 필터링 (예: `SQUAREBOOK_HC`) | | `category` | 카테고리로 필터링 (예: `album`, `diary`) | | `templateKind` | 종류로 필터링 (`cover`, `content`, `divider`, `publish`) | | `templateName` | 템플릿 이름 검색 — 띄어쓰기로 구분된 다중 키워드 AND 매칭 (예: `알림장 내지`) | | `specProfileUid` | SpecProfile UID로 필터링 | | `theme` | 테마로 필터링 (예: `spring`, `minimal`) | | `sort` | 정렬 기준 — `name_asc`, `name_desc`, `created_asc`, `updated_desc`, `updated_asc` (기본: `created_desc`) | | `limit`, `offset` | 페이지네이션 (기본 limit: 50) | **Request** ```bash # SQUAREBOOK_HC용 내지 템플릿 조회 curl 'https://api-sandbox.sweetbook.com/v1/templates?bookSpecUid=SQUAREBOOK_HC&templateKind=content' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Response** ```json { "success": true, "message": "Success", "data": [ { "templateUid": "8hKvbcNJdSgj", "accountUid": "public", "templateName": "알림장A_내지", "description": "알림장 내지 — 미니멀 1단", "templateKind": "content", "category": "diary", "theme": "minimal", "bookSpecUid": "SQUAREBOOK_HC", "bookSpecName": "고화질 스퀘어북 (하드커버)", "isPublic": true, "status": "active", "thumbnails": { "layout": "https://..." }, "createdAt": "2026-01-15T09:00:00Z", "updatedAt": "2026-01-20T14:30:00Z" } ], "pagination": { "total": 45, "limit": 50, "offset": 0, "hasNext": false } } ``` ### 상세 조회 `GET /templates/{templateUid}`로 특정 템플릿의 상세 정보를 확인할 수 있습니다. 상세 응답에는 파라미터 정의, 레이아웃, 레이아웃 규칙, 베이스 레이어 등이 포함됩니다. | 상세 응답 필드 | 설명 | | --- | --- | | `parameters` | 파라미터 정의 (텍스트/이미지/갤러리 바인딩 정보) | | `layout` | 레이아웃 정의 (요소 배치, 크기) | | `layoutRules` | 레이아웃 규칙 (여백, 흐름, 컬럼 등) | | `baseLayer` | 베이스 레이어 (홀수/짝수 페이지 배경 요소) | | `thumbnails` | 템플릿 외형 견본 URL 객체. `layout` / `baseLayerOdd` / `baseLayerEven` 세 키가 있으며, 등록되지 않은 항목은 응답에서 omit됩니다 (셋 모두 미등록이면 `thumbnails` 객체 자체가 omit). 응답에 키가 존재하면 해당 URL은 항상 유효합니다. | > ℹ️ **썸네일은 사전 등록된 외형 견본입니다.** 파트너가 입력한 사진이 합성된 결과가 아니며, 카탈로그 UI나 템플릿 선택 화면 표시용으로만 사용합니다. 입력 사진이 실제로 합성된 결과는 책 최종화(`POST /books/{bookUid}/finalization`) 후 PDF 다운로드로만 확인할 수 있습니다. 썸네일 URL은 **정적 호스팅 자산**이며 다음 정책을 따릅니다. - **인증 불요**: `Authorization` 헤더 없이 접근 가능합니다. ``에 직접 사용할 수 있습니다. - **영구 URL**: 만료·서명 토큰이 없습니다. 캐시·CDN 친화적입니다. - **확장자 가변**: `.jpg` / `.png` / `.webp` 등 등록 시 원본 확장자가 그대로 유지됩니다. 응답의 URL을 그대로 사용하고 확장자를 임의로 가정·변경하지 마세요. - **도메인**: 호출 도메인(`api-sandbox.sweetbook.com` / `api.sweetbook.com`)이 자동 적용됩니다. **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/templates/8hKvbcNJdSgj' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **thumbnails 응답 예시 (3종 모두 등록된 경우)** ```json "thumbnails": { "layout": "https://api-sandbox.sweetbook.com/templates_thumb/{templateUid}/layout.jpg", "baseLayerOdd": "https://api-sandbox.sweetbook.com/templates_thumb/{templateUid}/base_layer_odd.jpg", "baseLayerEven": "https://api-sandbox.sweetbook.com/templates_thumb/{templateUid}/base_layer_even.jpg" } ``` **thumbnails 응답 예시 (layout만 등록된 경우)** ```json "thumbnails": { "layout": "https://api-sandbox.sweetbook.com/templates_thumb/{templateUid}/layout.jpg" } ``` ## 템플릿 선택 기준 템플릿을 선택할 때 다음 사항을 확인하세요. #### 1. bookSpecUid 일치 템플릿의 `bookSpecUid`가 생성한 책의 `bookSpecUid`와 일치해야 합니다. 레이아웃 크기가 다르면 적용할 수 없습니다. #### 2. templateKind 확인 표지에는 `cover` 템플릿, 내지에는 `content` 템플릿을 사용해야 합니다. #### 3. 파라미터 확인 상세 조회로 `parameters`를 확인하여 어떤 값(텍스트, 사진 등)을 전달해야 하는지 파악하세요. ## 파라미터 구조 템플릿 상세 응답의 `parameters`는 `definitions` 래퍼로 감싸진 구조입니다. 각 파라미터 키는 `parameters.definitions.{key}` 경로에 위치합니다. | 필드 | 설명 | | --- | --- | | `parameters.definitions` | 파라미터 정의 목록 객체 | | `parameters.definitions.{key}.binding` | 바인딩 타입 — `text`, `file`, `rowGallery` | | `parameters.definitions.{key}.label` | 사람이 읽을 수 있는 파라미터 이름 | > ℹ️ 파라미터 필드를 식별할 때 `type` 대신 `binding` 필드를 사용합니다. > 플레이스홀더 문법은 `$$variableName$$` (달러 기호 두 개)입니다. 예: `$$bookTitle$$` **parameters 구조 예시** ```json { "parameters": { "definitions": { "bookTitle": { "binding": "text", "label": "책 제목" }, "frontPhoto": { "binding": "file", "label": "표지 사진" }, "photos": { "binding": "rowGallery", "label": "내지 사진 목록" } } } } ``` ## 파라미터 바인딩 템플릿의 `parameters.definitions`에 정의된 바인딩 유형에 따라 값을 전달합니다. | 바인딩 유형 | 전달 값 | 예시 | | --- | --- | --- | | `text` | 텍스트 문자열 | `"bookTitle": "우리 가족 앨범"` | | `file` | 이미지 URL 또는 업로드 파일 1장 | `"frontPhoto": "https://..."` | | `gallery` | 이미지 URL 또는 파일 배열 (다중) | `"images": ["a.jpg", "b.jpg"]` | | `collageGallery` | 콜라주용 이미지 배열 (다중) | `"collagePhotos": ["a.jpg", "b.jpg"]` | | `rowGallery` | 행 배치용 이미지 배열 (다중) | `"photos": ["photo1.jpg", "photo2.jpg"]` | > ⚠️ 갤러리 계열(`gallery` / `collageGallery` / `rowGallery`)에 사용할 사진은 먼저 `POST /books/{bookUid}/photos`로 업로드한 후, 반환된 `fileName`을 사용하거나 외부 URL을 직접 전달할 수 있습니다. ## 파라미터 스키마 조회 `GET /templates/{templateUid}/schema`는 템플릿 파라미터 명세를 **JSON Schema (draft-07) 형식**으로 반환합니다. 상세 조회(`GET /templates/{templateUid}`)의 `parameters` 필드는 내부 저장 구조를 그대로 노출하지만, 본 엔드포인트는 표준 JSON Schema 형식으로 변환하여 페이로드 검증·자동화 도구·코드 생성기와 바로 연동할 수 있도록 합니다. 권한 정책은 **상세 조회와 동일**합니다 — 공용 템플릿(`isPublic: true`)은 누구나, 본인 소유 개인 템플릿은 본인만 조회 가능합니다. 존재하지 않거나 접근 권한이 없는 `templateUid`로 호출하면 `404 ERR_NOT_FOUND`가 반환됩니다. **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/templates/4MY2fokVjkeY/schema' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 구조 응답의 `data` 객체는 JSON Schema draft-07 사양을 따르며 다음 필드를 포함합니다. | 필드 | 타입 | 설명 | | --- | --- | --- | | `$schema` | string | JSON Schema dialect URI. 항상 `http://json-schema.org/draft-07/schema#` | | `$id` | string | 스키마 식별자 — 템플릿 UID | | `title` | string | 템플릿 이름 | | `description` | string | 템플릿 설명. 등록된 설명이 없는 경우 응답에서 omit됩니다 (빈 문자열로 노출되지 않음) | | `type` | string | 항상 `"object"` 고정 (템플릿 파라미터는 key-value 구조) | | `properties` | object | 파라미터 키 → 스키마 정의. 항목 구조는 다음 섹션 참조 | | `required` | string[] | 필수 파라미터 키 목록. 판정 규칙은 별도 섹션 참조 | | `x-templateKind` | string | 템플릿 종류 (`cover` / `content` / `divider` / `publish`) — JSON Schema 표준 외 확장 필드 | > ℹ️ `description`과 같이 값이 없으면 응답에서 **키 자체가 omit**되는 필드가 있습니다. 클라이언트는 키 존재 여부를 검사한 뒤 사용해야 합니다 (`"description" in data`). **Response (표지 템플릿 예시)** ```json { "success": true, "message": "성공", "data": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "4MY2fokVjkeY", "title": "표지", "type": "object", "properties": { "frontPhoto": { "type": "string", "description": "앞표지 배경 대형 사진", "format": "uri", "x-binding": "file" }, "backPhoto": { "type": "string", "description": "뒤표지 소형 사진", "format": "uri", "x-binding": "file" }, "dateRange": { "type": "string", "description": "날짜 범위", "x-binding": "text" }, "spineTitle": { "type": "string", "description": "책등 및 앞표지 세로 제목", "x-binding": "text" } }, "required": ["frontPhoto", "dateRange", "spineTitle"], "x-templateKind": "cover" } } ``` ### properties 항목 구조 각 파라미터는 표준 JSON Schema property로 표현되며, 원본 `binding` 값은 `x-binding` 확장 필드로 보존됩니다. 바인딩 유형별로 다음과 같이 매핑됩니다. | 원본 binding | type | format | items | x-binding | | --- | --- | --- | --- | --- | | `text` | `"string"` | — | — | `"text"` | | `file` | `"string"` | `"uri"` | — | `"file"` | | `gallery` | `"array"` | — | `{ "type": "string", "format": "uri" }` | `"gallery"` | | `collageGallery` | `"array"` | — | `{ "type": "string", "format": "uri" }` | `"collageGallery"` | | `rowGallery` | `"array"` | — | `{ "type": "string", "format": "uri" }` | `"rowGallery"` | `description`은 원본 정의에 명시된 경우에만 노출됩니다 (없으면 omit). 원본에 `enum` 후보값이 있으면 그대로 passthrough되며, 없으면 omit됩니다. **갤러리 항목 응답 예시** ```json "photos": { "type": "array", "description": "사진들 (0~N장)", "items": { "type": "string", "format": "uri" }, "x-binding": "rowGallery" } ``` ### required 판정 규칙 응답의 루트 `required` 배열은 다음 규칙에 따라 결정됩니다. - 원본 정의에 `required`가 **명시되지 않은 키는 자동으로 필수**(true)로 간주되어 응답 `required` 배열에 포함됩니다. - 원본 정의에 `required: false`가 **명시된 키만 선택 파라미터**로 처리되어 `required` 배열에서 제외됩니다. - 본 규칙은 응답 시점에만 정규화되며 원본 데이터에는 영향을 주지 않습니다. 기존 `GET /templates/{templateUid}` 상세 조회의 `parameters` 필드는 raw passthrough이므로 이 정규화가 적용되지 않습니다. ### 상세 조회와의 비교 상세 조회(`GET /templates/{templateUid}`)와 신규 스키마 조회의 차이는 다음과 같습니다. | 항목 | 상세 조회 `GET /templates/{uid}` | 스키마 조회 `GET /templates/{uid}/schema` | | --- | --- | --- | | 응답 형식 | 내부 저장 구조 (parameters / layout / baseLayer / thumbnails 포함) | JSON Schema draft-07 문서 | | `parameters` 처리 | 저장된 JSON을 그대로 passthrough | JSON Schema 형식으로 변환 (`properties` + `required`) | | `required` 정규화 | 원본 그대로 (명시되지 않은 키는 누락) | 명시되지 않은 키 자동 포함 (위 규칙) | | 페이로드 크기 | 크다 (layout · 썸네일 · baseLayer 포함) | 작다 (스키마 전용) | | 주 용도 | 템플릿 편집 UI · 카탈로그 · 썸네일 사용 | 페이로드 검증 · 자동화 도구 · 코드 생성기 | **기존 호출자는 변경할 필요가 없습니다.** 상세 조회 응답 구조는 그대로 유지됩니다. JSON Schema 형식이 필요한 클라이언트만 새 엔드포인트를 추가로 호출하면 됩니다. ### 권장 사용 흐름 스키마 응답을 활용하여 책에 템플릿을 적용하는 표준 흐름은 다음과 같습니다. 1. `GET /templates?bookSpecUid=...`로 사용 가능한 템플릿 목록을 조회합니다. 2. `GET /templates/{templateUid}/schema`로 파라미터 스키마를 받습니다. 3. `required` 배열로 필수 키를 확인하고, `properties.{key}.x-binding`으로 전달 형식을 결정합니다. - `"text"` → `parameters[key]`에 문자열 - `"file"` → `parameters[key]`에 이미지 URL 또는 업로드된 사진의 `fileName` - 갤러리 계열(`gallery` / `collageGallery` / `rowGallery`) → `parameters[key]`에 URL/파일명 배열 4. `POST /books/{bookUid}/cover` 또는 `POST /books/{bookUid}/contents`로 템플릿을 적용합니다. `$schema: "http://json-schema.org/draft-07/schema#"`이 명시되어 있으므로 **기성 JSON Schema 검증기**(예: ajv, python-jsonschema)에 응답을 그대로 입력하여 페이로드 검증을 자동화할 수 있습니다. `x-binding` 같은 확장 필드는 표준 검증기가 무시하므로 별도 설정이 필요하지 않습니다. ## 책에 템플릿 적용하기 책 생성부터 최종화까지의 전체 흐름에서 템플릿이 사용되는 과정입니다. 1. `POST /books` — 책 생성 (bookSpecUid 지정) 2. `POST /books/{bookUid}/photos` — 사진 업로드 (필요 시) 3. `POST /books/{bookUid}/cover` — 표지 적용 (cover 템플릿 + 파라미터) 4. `POST /books/{bookUid}/contents` — 내지 추가 (content 템플릿 + 파라미터) ← 반복 5. `POST /books/{bookUid}/finalization` — 최종화 ### 표지 적용 예시 표지 템플릿을 적용할 때는 templateUid, 파라미터(제목, 저자 등), 이미지 파일을 함께 전달합니다. **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "templateUid": "COVER_TEMPLATE_UID", "parameters": { "bookTitle": "우리 가족 앨범", "year": "2026" } }' ``` ### 내지 추가 예시 > ℹ️ 내지는 `POST /books/{bookUid}/contents`를 여러 번 호출하여 페이지를 계속 추가할 수 있습니다. 각 호출마다 다른 템플릿을 사용할 수도 있습니다. **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/books/{bookUid}/contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "templateUid": "8hKvbcNJdSgj", "parameters": { "date": "2026-03-01", "dayOfWeek": "월요일", "teacherComment": "오늘 하루도 잘 보냈습니다.", "photos": ["photo1.jpg", "photo2.jpg"] } }' ``` ## HTTP 상태 코드 Templates API의 모든 엔드포인트(`GET /templates`, `GET /templates/{templateUid}`, `GET /templates/{templateUid}/schema`, `GET /template-categories`)는 다음 상태 코드를 공통으로 반환합니다. | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 (결과 없으면 빈 배열) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | 쿼리 파라미터 형식 오류 (`limit`/`offset` 범위, 잘못된 `sort` 값 등) | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 존재하지 않는 `templateUid` (단건/스키마 조회 시) | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 실패 응답 6필드 shape(`success`·`errorCode`·`message`·`data`·`errors`·`fieldErrors`) 및 errorCode 분기 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참고하세요. ## 관련 문서 - [Books — 템플릿 기반 — — 책 생성, 표지/내지 추가, 최종화](/docs/api/books-template) - [템플릿 기반 시나리오 — — 책 생성부터 주문까지 단계별 흐름](/docs/guides/scenario-template) - [혼합 방식 시나리오 — — 표지=템플릿, 내지=PDF 흐름](/docs/guides/scenario-mix) - [BookSpecs API — — 판형 정보 (페이지 수 규칙 포함) 조회](/docs/api/book-specs) - [템플릿 구조와 바인딩 — — 변수, 파라미터, 이미지 매칭 동작 원리](/docs/concepts/template-structure) - [레이아웃 동작 원리 — — breakBefore, 페이지 흐름](/docs/concepts/layout-principles) - [갤러리 템플릿 — — 다수 이미지 배치 (collageGallery, rowGallery)](/docs/concepts/gallery) - [베이스 레이어 — — 표지·내지 공통 배경 레이어](/docs/concepts/base-layer) --- # PDF 표지/내지 사이즈 Book Print API가 제공하는 판형의 표지·내지 PDF 사이즈와 계산식을 안내합니다. > ℹ️ 본 페이지의 계산식·예시 수치는 PDF 업로드 시 검증 기준입니다. 업로드 검증은 **±1mm 톨러런스**로 적용되며, [사이즈 계산 API](/docs/concepts/pdf-size-api) 응답값으로 PDF를 만들면 검증 통과가 보장됩니다. 실제 업로드 동작은 > [Books — PDF 기반](/docs/api/books-pdf)을 참고하세요. ## 하드커버 표지 각 상품의 데이터를 더해 표지와 내지 판형을 계산합니다. ![하드커버 표지 영역 — 싸바리 영역, 뒤표지(내지 w·h), 미소, 책등, 미소, 앞표지(내지 w·h)](https://api.sweetbook.com/images/pdf-size/hardcover-cover.svg) ### 예시 — `SQUAREBOOK_HC` | 데이터 | 필드 | 치수 (mm) | | --- | --- | --- | | 내지w | `bookSpec.innerTrimWidthMm` | 243 | | 내지h | `bookSpec.innerTrimHeightMm` | 248 | | 책등1 (24~64p) | `bookSpec.spineWidthRules[0].spineWidthMm` | 10 | | 책등2 (65~130p) | `bookSpec.spineWidthRules[1].spineWidthMm` | 16 | | 미소 | `bookSpec.hingeGapMm` | 10 | | 싸바리 영역 | `bookSpec.caseWrapMarginMm` | 17 | | 도련 | `bookSpec.bleedMm` | 3 | ※ `caseWrapMarginMm`, `hingeGapMm`, `spineWidthRules`는 **하드커버 전용 필드**로, 소프트커버 spec 응답에서는 자동 제외됩니다. ### 하드커버 표지 계산식 **계산식** ```text 표지 w : (innerTrimWidthMm × 2) - (bleedMm × 2) + spineWidthMm + (hingeGapMm × 2) + (caseWrapMarginMm × 2) // (내지w × 2) - (도련 × 2) + 책등 + (미소 × 2) + (싸바리 영역 × 2) 표지 h : innerTrimHeightMm + (bleedMm × 2) + (caseWrapMarginMm × 2) // 내지h + (도련 × 2) + (싸바리 영역 × 2) ``` ※ `spineWidthMm`은 페이지 수가 속하는 `spineWidthRules` 구간의 값을 사용합니다. 예시) 스퀘어북 하드커버 24p 표지 = **544 × 288 (mm)** **계산 예시** ```text 표지 w : (243 × 2) - (3 × 2) + 10 + (10 × 2) + (17 × 2) = 544 표지 h : 248 + (3 × 2) + (17 × 2) = 288 ``` ### 예시 — `SQUAREBOOK_LAYFLAT_HC` ※ 표지 계산식과 데이터 필드는 `SQUAREBOOK_HC`와 동일하며, `spineWidthRules`의 페이지 구간(16~18p / 19~46p)만 다릅니다. | 데이터 | 필드 | 치수 (mm) | | --- | --- | --- | | 내지w | `bookSpec.innerTrimWidthMm` | 243 | | 내지h | `bookSpec.innerTrimHeightMm` | 248 | | 책등1 (16~18p) | `bookSpec.spineWidthRules[0].spineWidthMm` | 10 | | 책등2 (19~46p) | `bookSpec.spineWidthRules[1].spineWidthMm` | 16 | | 미소 | `bookSpec.hingeGapMm` | 10 | | 싸바리 영역 | `bookSpec.caseWrapMarginMm` | 17 | | 도련 | `bookSpec.bleedMm` | 3 | 예시) 스퀘어북 레이플랫 하드커버 16p 표지 = **544 × 288 (mm)** **계산 예시 (16~18p)** ```text 표지 w : (243 × 2) - (3 × 2) + 10 + (10 × 2) + (17 × 2) = 544 표지 h : 248 + (3 × 2) + (17 × 2) = 288 ``` 예시) 스퀘어북 레이플랫 하드커버 20p 표지 = **550 × 288 (mm)** **계산 예시 (19~46p)** ```text 표지 w : (243 × 2) - (3 × 2) + 16 + (10 × 2) + (17 × 2) = 550 표지 h : 248 + (3 × 2) + (17 × 2) = 288 ``` ## 소프트커버 표지 각 상품의 데이터를 더해 표지와 내지 판형을 계산합니다. ![소프트커버 표지 영역 — 도련, 내지(w·h), 책등, 내지(w·h)](https://api.sweetbook.com/images/pdf-size/softcover-cover.svg) ### 예시 — `PHOTOBOOK_A5_SC` | 데이터 | 필드 | 치수 (mm) | | --- | --- | --- | | 내지w | `bookSpec.innerTrimWidthMm` | 148 | | 내지h | `bookSpec.innerTrimHeightMm` | 210 | | 표지 두께 | `bookSpec.coverThicknessMm` | 1.0 | | 페이지 두께 | `bookSpec.pageThicknessMm` | 0.054 (1p 당) | | 도련 | `bookSpec.bleedMm` | 3 | ※ 표지 두께·페이지 두께는 사용되는 종이에 따라 값이 바뀝니다. 다른 종이를 사용할 경우 문의해 주세요. ※ `coverThicknessMm`, `pageThicknessMm`는 **소프트커버 전용 필드**로, 하드커버 spec 응답에서는 자동 제외됩니다. 모든 spec에 공통으로 포함되는 필드는 `bleedMm`뿐입니다. ### PUR 제본 책등 계산 **책등** ```text spineWidthMm : coverThicknessMm + (pageThicknessMm × pages) // 표지 두께 + (페이지 두께 × 전체 페이지 수) ``` 예시) A5 소프트커버 70p 책등 = **4.78 (mm)** **계산 예시** ```text spineWidthMm : 1.0 + (0.054 × 70) = 4.78 ``` 전체 페이지 수는 각 상품의 최소~최대 페이지 범위 안에서 설정 가능합니다. ### PUR 제본 표지 계산식 **계산식** ```text 표지 w : (innerTrimWidthMm × 2) + (bleedMm × 2) + spineWidthMm // (내지w × 2) + (도련 × 2) + 책등 표지 h : innerTrimHeightMm + (bleedMm × 2) // 내지h + (도련 × 2) ``` 예시) A4 소프트커버 70p의 표지 = **430.78 × 303 (mm)** **계산 예시** ```text 표지 w : (210 × 2) + (3 × 2) + (1.0 + 70 × 0.054) = 430.78 표지 h : 297 + (3 × 2) = 303 ``` ### 레이플랫 제본 표지 계산식 > ℹ️ 소프트 커버 레이플랫 제본은 문의해 주세요. ## 내지 사이즈 PUR 제본 내지는 **단면 기준**으로 제작되며, 레이플랫 제본 내지는 **스프레드 기준**으로 제작됩니다. ### PUR 제본 ![PUR 제본 내지 — 단면 기준 (도련 포함)](https://api.sweetbook.com/images/pdf-size/pur-inner.svg) **계산식** ```text 내지 w : innerTrimWidthMm + (bleedMm × 2) // 내지w + (도련 × 2) 내지 h : innerTrimHeightMm + (bleedMm × 2) // 내지h + (도련 × 2) ``` 예시) 하드커버 스퀘어북 내지 (PUR 제본) = **249 × 254 (mm)** **계산 예시** ```text 내지 w : 243 + (3 × 2) = 249 내지 h : 248 + (3 × 2) = 254 ``` ### 레이플랫 제본 ![레이플랫 제본 내지 — 스프레드 기준 (좌우 페이지 + 도련)](https://api.sweetbook.com/images/pdf-size/layflat-inner.svg) > ⚠️ **레이플랫 내지 PDF는 펼침면(spread) 형식입니다.** > > > PDF 1페이지가 책 2면(좌·우 펼침)에 해당합니다. 업로드할 PDF 페이지수는 책 페이지수의 절반입니다. **계산식** ```text 내지 w : (innerTrimWidthMm × 2) + (bleedMm × 2) // (내지w × 2) + (도련 × 2) 내지 h : innerTrimHeightMm + (bleedMm × 2) // 내지h + (도련 × 2) ``` 예시) 하드커버 스퀘어북 내지 (레이플랫 제본) = **492 × 254 (mm)** **계산 예시** ```text 내지 w : (243 × 2) + (3 × 2) = 492 내지 h : 248 + (3 × 2) = 254 ``` #### 업로드 PDF 페이지수 **페이지수 환산** ```text PDF 페이지수 = book.pageCount / 2 ``` | `book.pageCount` (책 페이지) | 업로드 PDF 페이지수 | | --- | --- | | 16 | 8 | | 20 | 10 | | 30 | 15 | | 46 | 23 | ※ `book.pageCount`, `bookSpec.pageMin`, `bookSpec.pageMax`, `bookSpec.pageIncrement`는 모두 **책 페이지 단위**로 저장됩니다. 업로드 검증 시점에만 PDF 페이지수로 환산됩니다. ## 용어 설명 | 용어 | 설명 | | --- | --- | | 내지 | 책 내부 페이지의 실제 작업 크기입니다. 콘텐츠가 배치되는 기준 영역입니다. | | 책등 | 책의 중앙 두께 영역입니다. 소프트 커버의 경우 페이지 수와 용지 두께에 따라 크기가 달라집니다. | | 미소 | 표지와 책등 사이의 접힘 여유 영역입니다. 책을 펼칠 때 표지가 자연스럽게 접히도록 하기 위한 제작 영역입니다. | | 싸바리 영역 | 하드커버 제작 시 표지를 안쪽으로 감싸 접착하는 영역입니다. 표지 마감 및 제작 공정을 위해 필요한 여유 영역입니다. | | 도련 | 재단 공정 시 오차로 인해 흰 여백이 발생하지 않도록 추가하는 인쇄 여유 영역입니다. 배경 이미지나 색상이 재단선 밖까지 자연스럽게 이어지도록 사용됩니다. | ## 관련 문서 - [사이즈 계산 API — — calculated-size 엔드포인트·검산 예시·클라이언트 통합 시나리오](/docs/concepts/pdf-size-api) - [Books — PDF 기반 — — PDF 업로드 검증 동작 (±1mm 톨러런스)](/docs/api/books-pdf) - [PDF 업로드 시나리오 — — 책 생성부터 주문까지 흐름](/docs/guides/scenario-pdf) - [BookSpecs API — — 판형 일람·페이지 범위·신규 사이즈 입력 필드(`bleedMm`·`spineWidthRules` 등) 응답 명세](/docs/api/book-specs) --- # 사이즈 계산 API 판형 UID와 페이지 수만 전달하면 PDF 캔버스 사이즈를 계산해 반환하는 API입니다. > ℹ️ 응답값 그대로 PDF 캔버스를 만들면 PDF 업로드 검증이 **±1mm 톨러런스**로 자동 통과됩니다. 사이즈 계산 공식·도식은 > [PDF 표지/내지 사이즈](/docs/concepts/pdf-size)를 참고하세요. ## 엔드포인트 GET `/book-specs/{bookSpecUid}/calculated-size?pages={int}` ### Request | 필드 | 위치 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | --- | | `bookSpecUid` | path | string | O | BookSpec UID (예: `SQUAREBOOK_HC`) | | `pages` | query | int | O | 책의 전체 페이지 수 (짝수, `pageMin` ≤ pages ≤ `pageMax`) | ### Request 예시 **Request — 소프트커버 70페이지** ```bash curl 'https://api-sandbox.sweetbook.com/v1/book-specs/PHOTOBOOK_A4_SC/calculated-size?pages=70' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Request — 하드커버 50페이지** ```bash curl 'https://api-sandbox.sweetbook.com/v1/book-specs/SQUAREBOOK_HC/calculated-size?pages=50' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `bookSpecUid` | string | 입력한 spec UID echo | | `pages` | int | 입력한 페이지 수 echo | | `unit` | string | 단위 (항상 `"mm"`) | | `coverWidthMm` | number | 표지 펼침면 PDF 캔버스 너비 | | `coverHeightMm` | number | 표지 펼침면 PDF 캔버스 높이 | | `innerWidthMm` | number | 내지 PDF 너비 (제본에 따라 단면/펼침면) | | `innerHeightMm` | number | 내지 PDF 높이 | | `spineWidthMm` | number | 책등 너비 (참고/시각화용) | | `pdfToleranceMm` | number | PDF 업로드 검증 허용 오차. 모든 width/height 값에 ± 적용 | ※ 모든 수치는 소수점 둘째 자리까지 반올림됩니다. ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "성공", "data": { "bookSpecUid": "PHOTOBOOK_A4_SC", "pages": 70, "unit": "mm", "coverWidthMm": 430.78, "coverHeightMm": 303, "innerWidthMm": 216, "innerHeightMm": 303, "spineWidthMm": 4.78, "pdfToleranceMm": 1 } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 계산 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `pages`가 0 이하·홀수·`pageMin` 미만·`pageMax` 초과, 하드커버 spec에서 페이지가 어느 `spineWidthRules` 구간에도 매칭 안 됨, 사이즈 계산 미지원 spec | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 존재하지 않는 `bookSpecUid` | ## 페이지 수 입력에 따른 응답 변화 페이지 수가 늘어나면 책등 너비가 변하고 그에 따라 표지 펼침면 너비도 함께 변합니다. 그 외 값은 페이지에 무관합니다. | 응답 필드 | 페이지 수 변경 시 | | --- | --- | | `coverWidthMm` | 변동 (책등 변동에 따라) | | `coverHeightMm` | 고정 | | `innerWidthMm` | 고정 | | `innerHeightMm` | 고정 | | `spineWidthMm` | 변동 | - **하드커버**: `spineWidthRules` 구간(tier) 단위로 점프 (예: 24~64p는 동일, 65p부터 다른 값) - **소프트커버**: 페이지 1장당 `pageThicknessMm`만큼 점진적 증가 ## 검산 예시 판형별로 페이지 수에 따라 본 API가 반환하는 값을 정리한 표입니다. 직접 계산식으로 검증할 때 참고하세요. ### `SQUAREBOOK_HC` (하드커버, PUR 제본 — `spineWidthRules` 구간별 변동) | pages | coverWidthMm | coverHeightMm | innerWidthMm | innerHeightMm | spineWidthMm | | --- | --- | --- | --- | --- | --- | | 24 | 544 | 288 | 249 | 254 | 10 | | 64 | 544 | 288 | 249 | 254 | 10 | | 66 | 550 | 288 | 249 | 254 | 16 | | 100 | 550 | 288 | 249 | 254 | 16 | | 130 | 550 | 288 | 249 | 254 | 16 | ※ 64p → 66p 시점에 `spineWidthRules` 구간이 바뀌어 표지 너비·책등 너비가 6mm 점프합니다. `coverHeightMm`·`innerWidthMm`·`innerHeightMm`은 페이지와 무관하게 고정. ### `SQUAREBOOK_LAYFLAT_HC` (하드커버, 레이플랫 제본 — 내지 펼침면) | pages (책 페이지) | coverWidthMm | coverHeightMm | innerWidthMm | innerHeightMm | spineWidthMm | | --- | --- | --- | --- | --- | --- | | 16 | 544 | 288 | 492 | 254 | 10 | | 18 | 544 | 288 | 492 | 254 | 10 | | 20 | 550 | 288 | 492 | 254 | 16 | | 30 | 550 | 288 | 492 | 254 | 16 | | 46 | 550 | 288 | 492 | 254 | 16 | ※ `pages` 쿼리 파라미터는 **책 페이지 단위 + 짝수**로 전달합니다. `innerWidthMm`은 펼침면 한 장의 캔버스 크기이며, 업로드할 PDF 페이지수는 `pages / 2`입니다. 18p → 20p 사이에 `spineWidthRules` 구간이 바뀌어 표지 너비·책등 너비가 점프합니다. **Response (200 OK) — SQUAREBOOK_LAYFLAT_HC, pages=20** ```json { "success": true, "message": "성공", "data": { "bookSpecUid": "SQUAREBOOK_LAYFLAT_HC", "pages": 20, "unit": "mm", "coverWidthMm": 550, "coverHeightMm": 288, "innerWidthMm": 492, "innerHeightMm": 254, "spineWidthMm": 16, "pdfToleranceMm": 1 } } ``` ### `PHOTOBOOK_A4_SC` (소프트커버, PUR 제본 — 페이지 두께 누적) | pages | coverWidthMm | coverHeightMm | innerWidthMm | innerHeightMm | spineWidthMm | | --- | --- | --- | --- | --- | --- | | 24 | 428.30 | 303 | 216 | 303 | 2.30 | | 30 | 428.62 | 303 | 216 | 303 | 2.62 | | 50 | 429.70 | 303 | 216 | 303 | 3.70 | | 70 | 430.78 | 303 | 216 | 303 | 4.78 | | 100 | 432.40 | 303 | 216 | 303 | 6.40 | | 130 | 434.02 | 303 | 216 | 303 | 8.02 | ※ 페이지마다 `pageThicknessMm`(0.054)만큼 책등·표지 너비가 점진적으로 증가합니다. ### `PHOTOBOOK_A5_SC` (소프트커버, PUR 제본 — `pageMin=50`) | pages | coverWidthMm | coverHeightMm | innerWidthMm | innerHeightMm | spineWidthMm | | --- | --- | --- | --- | --- | --- | | 50 | 305.70 | 216 | 154 | 216 | 3.70 | | 70 | 306.78 | 216 | 154 | 216 | 4.78 | | 100 | 308.40 | 216 | 154 | 216 | 6.40 | | 130 | 310.02 | 216 | 154 | 216 | 8.02 | ※ A5 SC는 `pageMin=50`이므로 50p 미만 요청 시 400 응답을 반환합니다. ## ±1mm 톨러런스 PDF 업로드 검증의 expected 사이즈는 본 API 응답과 동일한 출처를 사용합니다. 응답값 그대로 PDF를 만들면 검증이 반드시 통과하며, ±1mm 이내의 작은 오차도 허용됩니다. **검증 실패 시 응답 메시지 예시** ```text PDF 너비 불일치: 실제 432.0mm, 허용 규격 430.78mm (+/-1mm) ``` ## 클라이언트 통합 시나리오 파트너 서비스에서 본 API를 어떻게 활용하는지 흐름별로 안내합니다. 사이즈 계산 공식·도식은 자매 페이지 [PDF 표지/내지 사이즈](/docs/concepts/pdf-size)를 참고하세요. ### 시나리오 A — 자체 PDF를 만들어 업로드 가장 일반적인 흐름. 파트너 서비스가 사용자 데이터를 받아 PDF를 직접 생성하고 업로드합니다. 1. **판형/페이지 수 결정** — 사용 spec(예: `PHOTOBOOK_A4_SC`)과 페이지 수(예: 70p) 확정 2. **사이즈 계산 API 호출** — `GET /book-specs/{bookSpecUid}/calculated-size?pages=70` 응답: `{ coverWidthMm: 430.78, coverHeightMm: 303, innerWidthMm: 216, innerHeightMm: 303, spineWidthMm: 4.78, pdfToleranceMm: 1 }` 3. **응답 사이즈로 PDF 캔버스 만들고 디자인 채움** - 표지: 430.78 × 303 mm (펼침면) - 내지: 216 × 303 mm × 70페이지 (PUR 제본 → 단면 PDF) 4. **PDF 업로드** — `POST /books/{bookUid}/pdf-cover`·`POST /books/{bookUid}/pdf-contents` 검증은 자동으로 calculated-size와 동일한 사이즈로 수행됩니다 (±1mm 톨러런스). 통과 → 200, 실패 → 400 + "PDF 너비 불일치" 메시지. 5. **책 최종화** — `POST /books/{bookUid}/finalization` 핵심: 2단계의 응답값 그대로 PDF를 만들면 4단계 검증 통과가 보장됩니다. ### 시나리오 B — 사이즈 미리보기 / 사용자 안내 파트너 서비스가 사용자에게 책의 인쇄 크기를 미리 보여줄 때 활용합니다. 1. 사용자가 페이지 수를 슬라이더로 조정 (예: 24p ↔ 100p) 2. 페이지 변경마다 `GET /book-specs/{uid}/calculated-size?pages={N}` 호출 또는 `GET /book-specs/{uid}`로 한 번만 받아서 클라이언트가 직접 계산 3. 사용자에게 표시: "당신의 책 크기: 가로 308.40mm × 세로 216mm" / "책등 두께: 6.40mm" 팁: 파트너 측에서 직접 계산하려면 `GET /book-specs/{uid}` 응답의 `bleedMm`·`coverThicknessMm`·`pageThicknessMm` 등을 받아 자매 페이지의 계산식을 적용하세요. ### 시나리오 C — 사용자 PDF 업로드 사전 검증 사용자가 자체 PDF를 가져와 업로드 시도하는 경우, 파트너 서비스가 업로드 전에 사이즈를 미리 검증합니다. 1. 사용자 PDF 업로드 → 파트너 서비스가 PDF 크기 추출 (예: 432 × 303) 2. `GET /book-specs/{uid}/calculated-size?pages=70` 호출 응답: `{ coverWidthMm: 430.78, pdfToleranceMm: 1, ... }` 3. 파트너 서비스에서 검증: `|userPdfWidth - coverWidthMm| ≤ pdfToleranceMm`이면 업로드 진행, 아니면 "사이즈가 안 맞습니다. 가이드: 430.78 × 303 (±1mm)" 안내 4. 통과 시 SweetBook API에 업로드 → 동일 검증으로 한 번 더 통과 보장 ## 자주 묻는 질문 ### Q: 페이지 수를 변경할 때마다 API를 호출해야 하나요? 아니요. `GET /book-specs/{uid}` 응답의 `bleedMm`·`coverThicknessMm`·`pageThicknessMm`·`spineWidthRules` 등을 받아두면 클라이언트에서 직접 계산할 수 있습니다. 다만 정확한 값은 본 API의 라운딩(소수점 둘째 자리)과 일치해야 검증 통과가 보장됩니다. ### Q: PDF가 톨러런스(±1mm)를 살짝 초과하면 어떻게 되나요? 업로드 검증에서 400 응답 + `PDF 너비 불일치: 실제 X mm, 허용 규격 Y mm (+/-1mm)` 메시지가 반환됩니다. ### Q: 페이지 수가 spec의 `pageMin` 미만이면? 본 API 호출 시 `400 ERR_VALIDATION_FAILED` + `페이지 수가 최소값(N)보다 작습니다` 메시지. spec마다 `pageMin`이 다릅니다 (예: A4 SC=24, A5 SC=50). ### Q: 페이지 수는 짝수만 되나요? 네. 책 자체가 짝수 페이지로만 만들어지므로 홀수 입력 시 400 응답이 반환됩니다. ### Q: BookSpec 응답에 일부 필드가 없는 spec이 있어요. spec의 `coverType`·`bindingType`에 따라 무관 필드는 응답에서 자동 제외됩니다. 하드커버 응답에는 `caseWrapMarginMm`·`hingeGapMm`·`spineWidthRules`가 포함되고 `coverThicknessMm`·`pageThicknessMm`은 제외됩니다 (소프트커버는 반대). 모든 spec에 공통으로 포함되는 필드는 `bleedMm`뿐입니다. ## 관련 문서 - [PDF 표지/내지 사이즈 — — 도식·계산식·하드/소프트 분기](/docs/concepts/pdf-size) - [Books — PDF 기반 — — PDF 업로드 검증 동작](/docs/api/books-pdf) - [BookSpecs API — — 판형 일람·페이지 범위·신규 사이즈 입력 필드 응답 명세](/docs/api/book-specs) - [PDF 업로드 시나리오 — — 책 생성부터 주문까지 흐름](/docs/guides/scenario-pdf) --- # 템플릿 구조와 바인딩 템플릿의 파라미터 구조를 이해하고, API 호출 시 텍스트·이미지를 올바르게 바인딩하는 방법을 설명합니다. ## 파라미터 구조 각 템플릿은 `parameters.definitions`에 바인딩 변수 목록을 정의합니다. 템플릿 조회 API(`GET /templates/{templateUid}`)로 구조를 확인할 수 있습니다. ### 변수 속성 | 속성 | 타입 | 설명 | | --- | --- | --- | | `binding` | string | 바인딩 유형. `text` (텍스트), `file` (이미지 파일), `collageGallery` (콜라주 갤러리), `rowGallery` (행 갤러리) | | `type` | string | 값 타입. `string`, `int`, `boolean`, `double`, `array` | | `required` | boolean | `true`이면 필수. 누락 시 400 에러 | | `description` | string | 변수 설명 | **표지 템플릿 parameters 예시 (79yjMH3qRPly)** ```json { "definitions": { "coverPhoto": { "binding": "file", "type": "string", "required": true, "description": "앞표지 메인 사진" }, "title": { "binding": "text", "type": "string", "required": true, "description": "책 제목 (예: 나의 하루 기록)" }, "dateRange": { "binding": "text", "type": "string", "required": true, "description": "날짜 범위 (예: 26.01 - 27.03)" } } } ``` ## 바인딩 유형별 전달 방법 ### text 바인딩 `parameters` JSON 문자열에 키-값으로 텍스트를 전달합니다. ### file 바인딩 이미지를 전달하는 방식입니다. 두 가지 방법 중 선택: | 방식 | parameters 값 | form 필드 | | --- | --- | --- | | 파일 업로드 | `"$upload"` | 변수명과 동일한 필드에 파일 첨부 | | URL 전달 | 이미지 URL 문자열 | 불필요 | ### collageGallery / rowGallery 바인딩 여러 장의 사진을 갤러리 형태로 배치합니다. `type: "array"`이며 `itemType: "file"`로 이미지 배열을 전달합니다. 자세한 내용은 [갤러리 템플릿](/docs/concepts/gallery) 페이지 참조. **parameters JSON 예시 — 텍스트 + 파일 업로드** ```json { "title": "나의 하루 기록", "dateRange": "26.01 - 26.04", "coverPhoto": "$upload" } ``` **parameters JSON 예시 — URL 전달** ```json { "title": "나의 하루 기록", "dateRange": "26.01 - 26.04", "coverPhoto": "https://example.com/front.jpg" } ``` ## 필수/선택 파라미터 | `required` 값 | 동작 | | --- | --- | | `true` | 필수 — 누락 시 400 에러 | | `false` 또는 미정의 | 선택 — 누락 시 해당 요소가 페이지에서 제거됨 | 선택 파라미터를 생략하면 해당 요소가 렌더링 결과에서 자동으로 숨겨집니다. 의도적으로 특정 요소를 표시하지 않고 싶을 때 활용할 수 있습니다. **내지 템플릿 parameters 예시 (46VqZhVNOfAp)** ```json { "definitions": { "monthNum": { "binding": "text", "type": "string", "required": true, "description": "월 번호 2자리 (예: 04)" }, "dayNum": { "binding": "text", "type": "string", "required": true, "description": "일 번호 2자리 (예: 25)" }, "diaryText": { "binding": "text", "type": "string", "required": true, "description": "일기 본문 텍스트" }, "photo": { "binding": "file", "type": "string", "required": true, "description": "메인 사진" } } } ``` ## 템플릿 종류 (templateKind) | 종류 | 용도 | 사용 API | | --- | --- | --- | | `cover` | 표지 | `POST /books/{bookUid}/cover` | | `content` | 내지 (본문 페이지) | `POST /books/{bookUid}/contents` | | `divider` | 간지 (구분 페이지) | `POST /books/{bookUid}/contents` | | `publish` | 발행면 | `POST /books/{bookUid}/contents` | 템플릿 조회 시 `templateKind` 파라미터로 종류를 필터링할 수 있습니다. --- # 레이아웃 동작 원리 내지 추가 시 콘텐츠가 페이지에 어떻게 배치되는지, `breakBefore` 파라미터로 배치를 어떻게 제어하는지 설명합니다. ## 페이지 배치 기본 원리 내지 API(`POST /books/{bookUid}/contents`)를 호출할 때마다 콘텐츠가 순서대로 배치됩니다. 각 콘텐츠의 배치 위치는 `breakBefore` 쿼리 파라미터로 제어합니다. ### breakBefore 옵션 | 값 | 동작 | 기본값 | | --- | --- | --- | | `page` | 항상 새로운 페이지에서 시작 | `divider`, `publish` 종류의 기본값 | | `column` | 현재 페이지에 여유 컬럼이 있으면 그 컬럼에 배치, 없으면 다음 페이지 | — | | `none` | 이전 콘텐츠 바로 다음에 연속 배치 | `content` 종류의 기본값 | **내지 추가 응답 — 배치 결과** ```json { "success": true, "message": "Content created successfully", "data": { "result": "inserted", "breakBefore": "page", "pageNum": 1, "pageSide": "right", "pageCount": 0 } } ``` ## 페이지 구성 책의 각 페이지는 **왼쪽(left)**과 **오른쪽(right)** 면으로 구성됩니다. 내지 추가 응답의 `pageSide` 필드로 콘텐츠가 어느 면에 배치되었는지 확인할 수 있습니다. ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `pageNum` | int | 콘텐츠가 배치된 페이지 번호 | | `pageSide` | string | `left` 또는 `right` | | `pageCount` | int | 현재까지 총 페이지 수 | ## 컬럼 레이아웃 일부 템플릿은 한 페이지 안에 여러 컬럼을 가질 수 있습니다. `breakBefore=column`을 사용하면 같은 페이지 내에서 다음 컬럼으로 이동하여 배치합니다. 모든 컬럼이 사용된 경우 다음 페이지로 넘어갑니다. ### 사용 예시 2컬럼 템플릿에서 첫 번째 콘텐츠는 왼쪽 컬럼에, 두 번째 콘텐츠는 `breakBefore=column`으로 오른쪽 컬럼에 배치할 수 있습니다. ## 판형별 시작 위치 판형에 따라 첫 내지 페이지의 시작 위치가 다를 수 있습니다. 예를 들어 SQUAREBOOK_HC는 첫 내지 페이지가 **오른쪽(right)**부터 시작합니다. 내지 추가 응답의 `pageSide` 값으로 실제 배치 결과를 확인하세요. --- # 갤러리 템플릿 여러 장의 사진을 한 영역에 자동 배치하는 갤러리 바인딩의 사용법을 설명합니다. ## 갤러리 바인딩 종류 템플릿의 `parameters.definitions`에서 `binding` 값이 갤러리 타입인 변수가 있으면, 해당 변수에 이미지 배열을 전달합니다. | binding 값 | 배치 방식 | 사진 수 | | --- | --- | --- | | `collageGallery` | 사진 비율을 분석해 콜라주 형태로 자동 배치 | 1~9장 | | `rowGallery` | 사진을 행 단위로 자동 배치 | 1장 이상 | **갤러리 템플릿 parameters 예시 (6c2HU8tipz1l)** ```json { "definitions": { "monthNum": { "binding": "text", "type": "string", "required": true, "description": "월 번호 2자리 (예: 04)" }, "dayNum": { "binding": "text", "type": "string", "required": true, "description": "일 번호 2자리 (예: 25)" }, "collagePhotos": { "binding": "collageGallery", "type": "array", "itemType": "file", "minItems": 1, "maxItems": 9, "required": true, "description": "콜라주 갤러리 사진들" } } } ``` ## 이미지 전달 방법 갤러리 변수에 이미지를 전달할 때는 `parameters` JSON의 해당 변수에 **배열**로 값을 지정합니다. 배열 내 각 항목은 파일 업로드(`"$upload"`), URL, 또는 서버에 이미 업로드된 파일명을 혼합 사용할 수 있습니다. ### 파일 업로드 방식 갤러리 변수명과 동일한 필드명으로 여러 파일을 전송합니다. `parameters`에서는 `"$upload"`를 순서대로 나열합니다. ### URL 방식 `parameters`의 배열에 이미지 URL을 직접 지정합니다. ### 갤러리 변수의 추가 속성 | 속성 | 타입 | 설명 | | --- | --- | --- | | `itemType` | string | 배열 항목 타입 (`file`) | | `minItems` | int | 최소 사진 수 | | `maxItems` | int | 최대 사진 수 | **collageGallery — 파일 업로드** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/contents?breakBefore=page' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=6c2HU8tipz1l' \\ -F 'collagePhotos=@photo1.jpg;type=image/jpeg' \\ -F 'collagePhotos=@photo2.jpg;type=image/jpeg' \\ -F 'collagePhotos=@photo3.jpg;type=image/jpeg' \\ -F 'parameters={"monthNum":"04","dayNum":"25","collagePhotos":["$upload","$upload","$upload"]}' ``` **collageGallery — URL 전달** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/contents?breakBefore=page' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=6c2HU8tipz1l' \\ -F 'parameters={"monthNum":"04","dayNum":"25","collagePhotos":["https://example.com/1.jpg","https://example.com/2.jpg","https://example.com/3.jpg"]}' ``` **rowGallery — 파일 업로드** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/contents?breakBefore=page' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=1vuzMfUnCkXS' \\ -F 'photos=@photo1.jpg;type=image/jpeg' \\ -F 'photos=@photo2.jpg;type=image/jpeg' \\ -F 'photos=@photo3.jpg;type=image/jpeg' \\ -F 'parameters={"monthYearLabel":"2026 APRIL","photos":["$upload","$upload","$upload"]}' ``` **rowGallery — URL 전달** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/{bookUid}/contents?breakBefore=page' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F 'templateUid=1vuzMfUnCkXS' \\ -F 'parameters={"monthYearLabel":"2026 APRIL","photos":["https://example.com/1.jpg","https://example.com/2.jpg","https://example.com/3.jpg"]}' ``` --- # 베이스 레이어 템플릿에 페이지별 헤더/푸터를 자동으로 적용하는 기능입니다. 콘텐츠 아래에 깔리는 배경 레이어로, 모든 페이지에 일관된 헤더/푸터/장식 요소를 자동 적용합니다. ## 개요 | 특징 | 설명 | | --- | --- | | 페이지별 자동 적용 | 콘텐츠가 여러 페이지에 걸쳐 분할되면 각 페이지에 맞는 baseLayer가 자동 적용 | | 홀수/짝수 페이지 분리 | 책의 좌우 페이지에 서로 다른 레이아웃 적용 가능 | | 시스템 변수 지원 | 페이지 번호, 날짜 등 동적 값 자동 치환 | | 사용자 파라미터 바인딩 | `$$변수명$$` 형식으로 사용자 입력 값 바인딩 | ## 구조 ### 페이지 규칙 | 페이지 번호 | 물리적 위치 | baseLayer | | --- | --- | --- | | 1 | 오른쪽 (첫 페이지) | odd | | 2 | 왼쪽 | even | | 3 | 오른쪽 | odd | | 4 | 왼쪽 | even | **Layout definition** ```json { "baseLayer": { "odd": { "elements": [ /* 홀수 페이지(오른쪽)용 요소 */ ] }, "even": { "elements": [ /* 짝수 페이지(왼쪽)용 요소 */ ] } } } ``` ## 요소 정의 각 요소에는 고유한 `element_id`가 필요합니다. 접두사 규칙: `bl-odd-*` (홀수 페이지용), `bl-even-*` (짝수 페이지용). ### 예시: 헤더/푸터 **Element definition** ```json { "baseLayer": { "odd": { "elements": [ { "element_id": "bl-odd-header", "type": "text", "position": { "x": 1028, "y": 25 }, "width": 878, "height": 35, "text": "$$headerTitle$$ | 페이지 @@pageNum@@", "fontFamily": "Malgun Gothic", "fontSize": 12.0, "textAlignment": "Right", "verticalAlignment": "Center" }, { "element_id": "bl-odd-footer", "type": "text", "position": { "x": 1028, "y": 960 }, "width": 878, "height": 25, "text": "@@datetime.year@@년 @@datetime.month@@월 | 오른쪽 페이지", "textAlignment": "Right" } ] }, "even": { "elements": [ { "element_id": "bl-even-header", "type": "text", "position": { "x": 50, "y": 25 }, "width": 878, "height": 35, "text": "페이지 @@pageNum@@ | $$headerTitle$$", "fontFamily": "Malgun Gothic", "fontSize": 12.0, "textAlignment": "Left", "verticalAlignment": "Center" }, { "element_id": "bl-even-footer", "type": "text", "position": { "x": 50, "y": 960 }, "width": 878, "height": 25, "text": "왼쪽 페이지 | @@datetime.year@@년 @@datetime.month@@월", "textAlignment": "Left" } ] } } } ``` ## 변수 바인딩 ### 사용자 파라미터 ($$변수명$$) 템플릿의 `parameters`에 정의된 값으로 치환됩니다. API 호출 시 `"headerTitle": "나의 포토북"`을 전달하면 `$$headerTitle$$`이 `나의 포토북`으로 치환됩니다. ### 시스템 변수 (@@변수명@@) | 변수 | 설명 | 예시 | | --- | --- | --- | | `@@pageNum@@` | 실제 페이지 번호 | 1, 2, 3, ... | | `@@datetime.year@@` | 현재 연도 | 2026 | | `@@datetime.month@@` | 현재 월 | 1, 2, ..., 12 | | `@@datetime.date@@` | 현재 일 | 1, 2, ..., 31 | | `@@bookTitle@@` | 책 제목 | 나의 포토북 | ## 좌표 시스템 양페이지(spread) 기준 좌표입니다. 전체 너비: 약 1956px (bookSpec에 따라 다름). | 페이지 | X 좌표 범위 | 너비 | | --- | --- | --- | | 왼쪽 페이지 (even) | 50 ~ 928 | 978px | | 오른쪽 페이지 (odd) | 1028 ~ 1906 | 978px | ## 지원 요소 타입 baseLayer에서 사용 가능한 요소 타입: - `text`: 텍스트 요소 (헤더, 푸터, 페이지 번호 등) - `graphic`: 이미지/장식 요소 (라인, 로고 등) ## 완전한 템플릿 예시 **Template** ```json { "templateName": "헤더/푸터가 있는 본문 템플릿", "bookSpecUid": "SQUAREBOOK_HC", "templateKind": "content", "parameters": { "definitions": { "headerTitle": { "binding": "text", "type": "string", "required": false, "description": "헤더 제목" }, "contents": { "binding": "text", "type": "string", "required": true, "description": "본문 내용" } } }, "baseLayer": { "odd": { "elements": [ { "element_id": "bl-odd-header", "type": "text", "position": { "x": 1028, "y": 25 }, "width": 878, "height": 35, "text": "$$headerTitle$$ | 페이지 @@pageNum@@", "textAlignment": "Right" } ] }, "even": { "elements": [ { "element_id": "bl-even-header", "type": "text", "position": { "x": 50, "y": 25 }, "width": 878, "height": 35, "text": "페이지 @@pageNum@@ | $$headerTitle$$", "textAlignment": "Left" } ] } }, "layout": { "itemIndex": "0", "tag": "Print", "width": 424, "height": 860, "elements": [ { "element_id": "content-text", "type": "text", "position": { "x": 0, "y": 0 }, "width": 424, "height": 800, "text": "$$contents$$", "splittable": true, "isDynamic": true } ] }, "layoutRules": { "space": "page", "margin": { "pageMargin": { "spine": 50, "fore": 50, "head": 80, "tail": 60 } }, "flow": { "columns": 2, "columnGap": 30 } } } ``` ## 주의사항 > ⚠️ **element_id 필수**: 모든 baseLayer 요소는 고유한 `element_id`가 필요합니다. > > **접두사 규칙**: `bl-odd-*`, `bl-even-*` 접두사를 사용하면 시스템이 중복 적용을 방지합니다. > > **좌표 주의**: odd(오른쪽) 요소는 x 좌표를 1028 이상으로, even(왼쪽) 요소는 50 근처로 설정해야 합니다. > > **콘텐츠 영역 고려**: 헤더/푸터 영역과 콘텐츠 영역이 겹치지 않도록 `layoutRules.margin`을 적절히 설정하세요. --- # Orders API 주문 생성 및 주문 관리 API 문서입니다. > ℹ️ 모든 Orders API는 인증이 필요합니다. API Key를 `Authorization: Bearer ` 헤더로 제공하세요. ## 주문 상태 주문 상태는 **문자열 enum**(`orderStatus`)과 **한글 표시 문자열**(`orderStatusDisplay`)로 제공됩니다. 클라이언트 분기는 `orderStatus`로, UI 표시는 `orderStatusDisplay`로 하세요. 항목 단위 상태도 동일 규칙(`itemStatus`·`itemStatusDisplay`)이 적용됩니다. Scope 컬럼은 해당 상태가 주문 전체(`Order`)·항목(`Item`)·양쪽(`Order·Item`) 중 어디에 나타나는지 표시합니다. | `orderStatus` (문자열) | `orderStatusDisplay` (한글) | Scope | 의미 | | --- | --- | --- | --- | | `PAID` | 결제완료 | Order·Item | 충전금 차감 완료 (초기 안정 상태) | | `PDF_READY` | PDF준비완료 | Item | 출력용 PDF 생성 완료 | | `CONFIRMED` | 제작확정 | Order·Item | 제작건 확정 (출력일 배정) | | `IN_PRODUCTION` | 제작중 | Order·Item | 인쇄·제본·포장 진행 중 | | `COMPLETED` | 제작완료 | Item | 항목 제작 완료 (개별) | | `PRODUCTION_COMPLETE` | 전체제작완료 | Order | 모든 항목 제작 완료 (발송 대기) | | `SHIPPED` | 발송완료 | Order·Item | 택배 발송됨 | | `DELIVERED` | 배송완료 | Order | 고객 수령 확인 | | `CANCELLED` | 취소 | Order·Item | 취소 (환불 없음) | | `CANCELLED_REFUND` | 취소환불 | Order | 취소 + 충전금 환불 (부분 취소 귀결 포함) | | `ERROR` | 오류 | Order·Item | 오류 상태 | ### 상태 전이 규칙 주문 상태는 정해진 흐름에 따라 전이됩니다. **진행 주체** 컬럼은 누가 전이를 트리거하는지 표기합니다. - **파트너** — 파트너가 직접 엔드포인트 호출 - **자동** — 시스템이 조건 충족 시 즉시 전이 (별도 호출·웹훅 없음) - **제작 처리** — SweetBook 측 제작·발송 단계 진행. 파트너는 웹훅 이벤트로 상태 변화를 수신 | FROM | TO | 진행 주체 | 파트너 엔드포인트 | 웹훅 이벤트 | | --- | --- | --- | --- | --- | | (신규) | `PAID` | 파트너 | `POST /orders` | `order.created` | | `PAID` | `PDF_READY` | 자동 | — | — | | `PDF_READY` | `CONFIRMED` | 제작 처리 | — | `production.confirmed` | | `CONFIRMED` | `IN_PRODUCTION` | 제작 처리 | — | `production.started` | | `IN_PRODUCTION` | `COMPLETED` (항목) | 제작 처리 | — | — (항목 단위, 별도 이벤트 없음) | | `COMPLETED` (모든 항목) | `PRODUCTION_COMPLETE` | 자동 집계 | — | `production.completed` | | `PRODUCTION_COMPLETE` | `SHIPPED` | 제작 처리 | — | `shipping.departed` | | `SHIPPED` | `DELIVERED` | 제작 처리 | — | `shipping.delivered` | | `PAID` / `PDF_READY` | `CANCELLED_REFUND` | 파트너 | `POST /orders/{uid}/cancel` (전체) `POST /orders/{uid}/items/{uid}/cancel` (부분) | `order.cancelled` `order.item_cancelled` | ### 파트너 가능 액션 | 액션 | 가능 상태 | 설명 | | --- | --- | --- | | 주문 전체 취소 | `PAID`, `PDF_READY` | 주문 전체 취소, 결제 금액 전액(배송비/포장비 포함) 환불 | | 주문 항목 부분 취소 | `PAID`, `PDF_READY` | 항목 단위로 취소, 남은 항목으로 재계산 → 차액 환불. 마지막 항목까지 취소되면 전체 취소로 자동 귀결 | | 배송지 변경 | `PAID`, `PDF_READY`, `CONFIRMED` | 발송 전까지만 배송지 변경 가능 | ## 주문 목록 조회 GET `/orders` 파트너의 주문 목록을 조회합니다. 기간 및 상태 필터를 조합하여 사용할 수 있습니다. ### Query 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | limit | int | - | 반환할 항목 수 (기본 20, 최대 100) | | offset | int | - | 건너뛸 항목 수 (기본 0) | | status | string | int | - | 주문 상태로 필터링. **문자열 enum**(예: `PAID`, `CANCELLED_REFUND`, 대소문자 무관) 또는 숫자 코드(예: `20`) 모두 수용 | | from | string | - | 조회 시작일 (ISO 8601, 예: 2026-01-01T00:00:00Z) | | to | string | - | 조회 종료일 (ISO 8601, 예: 2026-12-31T23:59:59Z) | ### Request 예시 **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/orders?limit=20&offset=0&status=PAID&from=2026-01-01T00:00:00Z&to=2026-12-31T23:59:59Z' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### Response 예시 **Response (v1.2 평탄화 구조)** ```json { "success": true, "message": "성공", "data": [ { "orderUid": "or_3eAx********", "accountUid": "u_6b0f********************", "accountName": "샘플출판사", "accountOrganizationName": null, "orderType": "NORMAL", "externalRef": "PARTNER-ORDER-001", "orderStatus": "PAID", "orderStatusDisplay": "결제완료", "totalAmount": 5000.00, "paidCreditAmount": 5500.00, "paymentMethod": "CREDIT", "itemCount": 1, "recipientName": "김영수", "isTest": true, "orderedAt": "2026-02-19T01:10:47.000Z", "createdAt": "2026-02-19T01:10:47.000Z" } ], "pagination": { "total": 42, "limit": 20, "offset": 0, "hasNext": true } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `status` 파라미터가 허용되지 않는 값 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## 주문 상세 조회 GET `/orders/{orderUid}` 특정 주문의 상세 정보를 조회합니다. 주문 항목, 배송 정보, 현재 상태가 모두 포함됩니다. ### Path 파라미터 | 파라미터 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `orderUid` | string | O | 조회할 주문 UID | ### Request 예시 **Request** ```bash curl -X GET 'https://api-sandbox.sweetbook.com/v1/orders/or_3eAx********' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### Response 예시 **Response** ```json { "success": true, "message": "성공", "data": { "orderUid": "or_3eAx********", "orderType": "NORMAL", "orderStatus": "PAID", "orderStatusDisplay": "결제완료", "externalRef": "PARTNER-ORDER-001", "totalProductAmount": 60400.00, "totalShippingFee": 3000.00, "totalPackagingFee": 0.00, "totalAmount": 63400.00, "paidCreditAmount": 69740.00, "paymentMethod": "CREDIT", "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06101", "address1": "서울시 강남구 테헤란로 123", "address2": "4층 401호", "orderedAt": "2026-02-19T01:10:47.000Z", "items": [ { "itemUid": "oi_aB3c********", "bookUid": "bk_abc123", "bookTitle": "우리 아이 성장앨범", "quantity": 1, "unitPrice": 60400.00, "itemAmount": 60400.00, "itemStatus": "PAID", "itemStatusDisplay": "결제완료" } ] } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 주문을 찾을 수 없음 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## 주문 생성 POST `/orders` FINALIZED 상태의 책을 대상으로 주문을 생성합니다. 충전금이 즉시 차감됩니다. > ℹ️ 충전금 이중 차감 방지를 위해 `Idempotency-Key` 헤더를 반드시 포함하세요. > 동일한 키로 재시도하면 이전 응답이 그대로 반환됩니다. 자세한 내용은 [API 공통 사항 > 멱등성](/docs/api/common)을 참고하세요. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | items | array | O | 주문 항목 목록 (최소 1개) | | items[].bookUid | string | O | 책 UID (FINALIZED 상태여야 함) | | items[].quantity | int | O | 수량 (1~100) | | shipping | object | O | 배송지 정보 | | shipping.recipientName | string | O | 수령인 (최대 100자) | | shipping.recipientPhone | string | O | 연락처 (최대 20자) | | shipping.postalCode | string | O | 우편번호 (최대 10자) | | shipping.address1 | string | O | 주소 (최대 200자) | | shipping.address2 | string | - | 상세주소 (최대 200자) | | shipping.memo | string | - | 배송 메모 (최대 200자) | | externalRef | string | - | 파트너 외부 참조 식별자 (최대 100자) | ### 처리 로직 1. 각 bookUid 유효성 검증 (존재, 파트너 소유, FINALIZED 상태) 2. 각 책의 bookSpecUid로 가격 계산 3. 배송비(3,000원/주문) + 상품금액 합산 4. 충전금 잔액 확인 후 차감 5. 주문 생성 ### Request 예시 **Request headers** ```bash Authorization: Bearer {YOUR_API_KEY} Content-Type: application/json Idempotency-Key: unique-request-id-12345 ``` **Request body** ```json { "items": [ { "bookUid": "bk_abc123", "quantity": 1 }, { "bookUid": "bk_def456", "quantity": 2 } ], "shipping": { "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06101", "address1": "서울시 강남구 테헤란로 123", "address2": "4층 401호", "memo": "부재시 경비실" }, "externalRef": "PARTNER-ORDER-001" } ``` ### Response 예시 **Response** ```json { "success": true, "message": "주문이 생성되었습니다", "data": { "orderUid": "or_3eAx********", "orderType": "NORMAL", "orderStatus": "PAID", "orderStatusDisplay": "결제완료", "isTest": true, "totalProductAmount": 60400.00, "totalShippingFee": 3000.00, "totalPackagingFee": 0.00, "totalAmount": 63400.00, "paidCreditAmount": 69740.00, "paymentMethod": "CREDIT", "creditBalanceAfter": 930260.00, "recipientName": "김영수", "recipientPhone": "010-0000-0000", "postalCode": "06101", "address1": "서울시 강남구 테헤란로 123", "address2": "4층 401호", "shippingMemo": "부재시 경비실", "orderedAt": "2026-02-19T01:10:47.000Z", "paidAt": "2026-02-19T01:10:47.000Z", "items": [ { "itemUid": "oi_aB3c********", "bookUid": "bk_abc123", "bookTitle": "우리 아이 성장앨범", "bookSpecUid": "bs_spec001", "bookSpecName": "포토북 A4", "quantity": 1, "pageCount": 24, "unitPrice": 60400.00, "itemAmount": 60400.00, "itemStatus": "PAID", "itemStatusDisplay": "결제완료" } ] } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 201 Created | — | 주문 생성 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | 유효성 검증 실패 (Book 미존재, 미FINALIZED 등) | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 402 Payment Required | `ERR_INSUFFICIENT_CREDIT` | 충전금 잔액 부족 | | 422 Unprocessable Entity | `ERR_IDEMPOTENCY_KEY_MISMATCH` | 동일 `Idempotency-Key`로 다른 본문 재요청 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | **402 충전금 부족**: `ERR_INSUFFICIENT_CREDIT`가 반환되며 `data`에 진단 객체(`required`·`balance`·`currency`)가 포함됩니다. 응답 6필드 shape, 다른 errorCode, 처리 가이드는 [에러 코드 & 트러블슈팅 — ERR_INSUFFICIENT_CREDIT](/docs/operations/errors#ERR_INSUFFICIENT_CREDIT)을 참조하세요. ## 주문 전체 취소 POST `/orders/{orderUid}/cancel` PAID 또는 PDF_READY 상태의 주문 전체를 취소합니다. 결제된 충전금은 배송비/포장비 포함 전액 환불됩니다. ### 취소 조건 - PAID 또는 PDF_READY 상태이고 NORMAL 유형의 주문만 취소 가능합니다 - 제작이 시작된 주문(CONFIRMED 이후)은 취소할 수 없습니다 - 취소 시 결제 금액이 충전금으로 전액 환불됩니다 ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | cancelReason | string | O | 취소 사유 (최대 500자) | ### Request 예시 **Request body** ```json { "cancelReason": "고객 변심" } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 전체 취소 성공 (변경된 주문 상세 정보 반환, `orderStatus: "CANCELLED_REFUND"`) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `cancelReason` 누락, 제작확정 이후 상태(`PAID`/`PDF_READY`가 아님), 이미 취소된 주문 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 주문 없음 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 일부 항목만 취소하려면 아래 **주문 항목 부분 취소** API를 사용하세요. ## 주문 항목 부분 취소 POST `/orders/{orderUid}/items/{itemUid}/cancel` 주문에 포함된 특정 항목을 단일 단위로 취소합니다. 한 번 호출에 한 항목씩 처리하며, 여러 항목을 취소하려면 반복 호출합니다. ### 동작 - **남은 항목으로 가격을 재계산**하여 차액을 충전금으로 환불합니다 - 부분 취소는 **주문 상태(`orderStatus`)를 변경하지 않습니다** — 남은 항목은 제작확정 등 다음 단계로 정상 진행됩니다 - 취소된 항목의 `itemStatus`는 `CANCELLED_REFUND`로 전이됩니다 - **마지막 항목까지 취소되면** 주문 전체가 자동으로 `CANCELLED_REFUND`로 전이되며(`fullCancel: true`), 배송비/포장비 포함 잔액이 환불됩니다 ### 취소 조건 - 주문이 NORMAL 유형이고 `PAID` 또는 `PDF_READY` 상태여야 합니다 - 대상 항목(`itemStatus`)도 `PAID` 또는 `PDF_READY`여야 하며, 이미 취소된 항목은 재취소할 수 없습니다 - 제작이 시작된 주문(CONFIRMED 이후)은 항목 부분 취소도 불가합니다 ### 여러 항목 취소 시 주의 여러 항목을 한 번에 취소하려면 **항목별로 순차 호출**하세요. `Promise.all` 같은 병렬 호출은 권장하지 않습니다 — 서버가 항목 상태와 주문 금액을 항목 단위로 직렬 재계산하므로, 병렬 호출은 락 경합·재계산 불일치를 유발할 수 있습니다. 네트워크 재시도 시에는 동일한 `Idempotency-Key`를 재사용해 중복 환불을 방지하세요. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | cancelReason | string | O | 취소 사유 (최대 500자) | ### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/orders/{orderUid}/items/{itemUid}/cancel' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: cancel-item-001' \\ -d '{ "cancelReason": "고객 요청 (1권 제외)" }' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `orderUid` | string | 대상 주문 UID | | `orderStatus` | string | 처리 후 주문 상태 (문자열 enum). 부분 취소 시 기존 상태 유지(`PAID` / `PDF_READY`), 전체 취소 귀결 시 `CANCELLED_REFUND` | | `orderStatusDisplay` | string | 주문 상태 한글 표시 문자열 | | `fullCancel` | boolean | 이번 호출로 전체 취소로 귀결됐는지 여부 (마지막 항목 취소 시 `true`) | | `refundAmount` | number | 이번 호출로 환불된 금액 (원, decimal) | | `refundTransactionId` | number | 이번 환불의 충전금 거래 ID. 환불액이 0이면 `null` | | `cancelledItem` | object | 이번 호출로 취소된 항목 정보 (`itemUid`, `bookUid`, `quantity`, `itemAmount`) | | `remainingItemUids` | string[] | 남은(살아있는) 항목의 `itemUid` 목록 | | `order` | object | 처리 후 주문 상세 (**주문 상세 조회**와 동일 스키마) | ### Response 예시 **Response (200 OK) — 부분 취소 (남은 항목 존재)** ```json { "success": true, "message": "선택한 항목이 부분 취소되었습니다", "data": { "orderUid": "or_2lnj********", "orderStatus": "PAID", "orderStatusDisplay": "결제완료", "fullCancel": false, "refundAmount": 110.00, "refundTransactionId": 784, "cancelledItem": { "itemUid": "oi_6bGM********", "bookUid": "bk_3lOO********", "quantity": 1, "itemAmount": 100.00 }, "remainingItemUids": ["oi_1Nrc********"], "order": { "orderUid": "or_2lnj********", "orderStatus": "PAID", "orderStatusDisplay": "결제완료", "totalAmount": 3110.00, "paidCreditAmount": 3420.00, "refundAmount": 110.00, "items": [ { "itemUid": "oi_6bGM********", "itemStatus": "CANCELLED_REFUND", "itemStatusDisplay": "취소환불" }, { "itemUid": "oi_1Nrc********", "itemStatus": "PAID", "itemStatusDisplay": "결제완료" } ] } } } ``` **Response (200 OK) — 마지막 항목 → 전체 취소 귀결** ```json { "success": true, "message": "마지막 항목까지 취소되어 주문이 전체 취소되었습니다", "data": { "orderUid": "or_184U********", "orderStatus": "CANCELLED_REFUND", "orderStatusDisplay": "취소환불", "fullCancel": true, "refundAmount": 3410.00, "refundTransactionId": 782, "cancelledItem": { "itemUid": "oi_34Hu********", "bookUid": "bk_2IyK********", "quantity": 1, "itemAmount": 100.00 }, "remainingItemUids": [], "order": { "orderStatus": "CANCELLED_REFUND", "cancelledAt": "2026-04-22T01:21:38.000Z" } } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 부분 취소 성공 (전체 취소 귀결 포함) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `cancelReason` 누락, 이미 취소된 항목, 주문에 포함되지 않은 `itemUid`, 제작확정 이후 상태(`PAID`/`PDF_READY`가 아님) 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 주문 또는 항목 없음 | | 422 Unprocessable Entity | `ERR_IDEMPOTENCY_KEY_MISMATCH` | 동일 `Idempotency-Key`로 다른 본문 재요청 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 모든 실패 응답은 6필드 shape(`success`·`errorCode`·`message`·`data`·`errors`·`fieldErrors`)를 따릅니다. `errors[0]`에는 사용자 표시용 한글 메시지가 포함됩니다(예: `이미 취소된 항목입니다: {itemUid}`, `주문에 포함되지 않은 항목입니다: {itemUid}`, `제작확정 이전(PAID/PDF_READY) 상태의 주문만 부분 취소할 수 있습니다.`). errorCode·응답 shape 상세는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참고하세요. ## 부분 취소 — 자주 묻는 질문 (FAQ) ### Q: 부분 취소 후 `orderStatus`가 `PAID` 그대로인 것이 맞나요? 네, 의도된 동작입니다. 부분 취소는 "일부 항목 제거"이지 주문 자체의 상태 전이가 아닙니다. 남은 항목은 정상적으로 제작확정 등 다음 단계로 진행될 수 있도록 `orderStatus`는 그대로 유지되며, 취소 이력은 항목별 `itemStatus === "CANCELLED_REFUND"`로 확인합니다. 마지막 항목까지 취소되면 그 시점에 주문 전체가 `CANCELLED_REFUND`로 전이됩니다. ### Q: `cancelledItem.itemAmount`와 `refundAmount`가 왜 다른가요? 두 값은 서로 다른 기준입니다. `cancelledItem.itemAmount`는 취소된 항목의 **순수 상품 금액**(`unitPrice × quantity`)으로, 주문 시점의 스냅샷 값입니다. `refundAmount`는 **남은 항목으로 재계산한 차액**으로, 포장비·배송비·부가세(10%) 차액까지 모두 반영된 이번 호출의 총 환불액입니다. 정산/회계 검증은 `refundAmount`를, 항목 단위 수치 비교는 `itemAmount`를 사용하세요. ### Q: 환불 금액이 예상과 다를 수 있나요? 환불액은 남은 항목을 기준으로 **호출 시점의 현재 가격 정책**으로 재계산합니다. 주문 생성 이후 해당 업체의 포장비·배송비 규칙이나 커스텀 가격이 변경되었다면 환불액이 달라질 수 있습니다. 정산 검증 시에는 응답의 `refundAmount`와 `order` 객체에 반환된 갱신된 금액 필드를 함께 확인하세요. 가격 정책 변경으로 환불액이 음수가 되는 경우에는 `400 Bad Request`가 반환됩니다. ### Q: 부분 취소 이력이 있는 주문인지 UI에서 어떻게 판단하나요? 주문 상세 조회(`GET /orders/{orderUid}`) 응답의 `items[]`에 `itemStatus === "CANCELLED_REFUND"`인 항목이 하나라도 있고 동시에 `orderStatus`가 `"PAID"` 또는 `"PDF_READY"`라면 "부분 취소 이력 있음"으로 판단할 수 있습니다. ### Q: 충전금 환불 이력은 어떻게 조회하나요? [`GET /credits/transactions`](/docs/api/credits)로 조회할 때 `reasonCode=13 (ORDER_MODIFY_REFUND)`인 거래가 부분 취소 환불입니다. `memo` 필드에 해당 `orderUid`가 포함되어 있어 주문과 역추적할 수 있습니다. ### Q: 같은 `Idempotency-Key`로 재호출하면 어떻게 되나요? 첫 요청의 응답을 그대로 반환합니다(중복 환불 없음). 네트워크 타임아웃 등으로 재시도할 때는 **반드시 동일한 키**를 사용하세요. 키를 새로 생성해 재호출하면 서버는 별개 요청으로 처리합니다. ### Q: 부분 취소 웹훅은 어떻게 받나요? 매 호출마다 [`order.item_cancelled`](/docs/api/webhook-events) 이벤트가 발송됩니다. 마지막 항목을 취소해 전체 취소로 귀결되면 그 시점에 기존 전체 취소 이벤트 `order.cancelled`가 **추가로** 발송됩니다(구독자 호환). 두 이벤트를 함께 구독하는 것을 권장합니다. ## 배송지 변경 PATCH `/orders/{orderUid}/shipping` PAID ~ CONFIRMED 상태에서 배송지를 변경할 수 있습니다. 발송(SHIPPED) 이후에는 변경이 불가합니다. 변경할 필드만 전달합니다. ### Request Body 지원 필드: `recipientName`, `recipientPhone`, `postalCode`, `address1`, `address2`, `shippingMemo` (변경할 필드만 부분 전달) | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `recipientName` | string | — | 수령인 (최대 100자) | | `recipientPhone` | string | — | 연락처 (최대 20자) | | `postalCode` | string | — | 우편번호 (최대 10자) | | `address1` | string | — | 주소 (최대 200자) | | `address2` | string | — | 상세주소 (최대 200자) | | `shippingMemo` | string | — | 배송 메모 (최대 200자) | ### Request 예시 **Request body** ```json { "recipientName": "김영수", "address1": "서울시 서초구 반포대로 100" } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 변경 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | 지원되지 않는 필드, 발송(`SHIPPED`) 이후 변경 시도 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 주문 없음 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## 관련 문서 - [주문 상태 흐름 — — 상태 전이 규칙·웹훅 매핑·취소 규칙·SLA](/docs/operations/order-status) - [Credits API — — 충전금 잔액·거래 내역 (차감/환불 트리거)](/docs/api/credits) - [Webhook Events — — order.created·shipping.* 등 이벤트 페이로드 상세](/docs/api/webhook-events) - [Webhooks API — — 웹훅 등록·서명 검증·재시도 정책](/docs/api/webhooks) - [에러 코드 & 트러블슈팅 — — ERR_INSUFFICIENT_CREDIT·ERR_VALIDATION_FAILED 등 errorCode 분기](/docs/operations/errors) --- # 주문 상태 흐름 주문의 전체 상태 흐름, 웹훅 이벤트 매핑, 취소 규칙, Sandbox 동작 차이를 안내합니다. ## 상태 흐름도 주문은 다음과 같은 순서로 상태가 전이됩니다. PAID → PDF_READY → CONFIRMED → IN_PRODUCTION → PRODUCTION_COMPLETE → SHIPPED → DELIVERED 파트너 취소: PAID / PDF_READY → CANCELLED_REFUND ## 상태 코드 전체 목록 주문 상태는 **문자열 enum**(`orderStatus`)과 **한글 표시 문자열**(`orderStatusDisplay`)로 응답에 포함됩니다. 항목 단위 상태도 동일 규칙(`itemStatus`·`itemStatusDisplay`)입니다. | `orderStatus` | `orderStatusDisplay` | Scope | 설명 | | --- | --- | --- | --- | | `PAID` | 결제완료 | Order·Item | 충전금 차감 완료 (초기 안정 상태) | | `PDF_READY` | PDF준비완료 | Item | 출력용 PDF 생성 완료 | | `CONFIRMED` | 제작확정 | Order·Item | 제작건 확정 (출력일 배정) | | `IN_PRODUCTION` | 제작중 | Order·Item | 인쇄·제본·포장 진행 중 | | `COMPLETED` | 제작완료 | Item | 항목 제작 완료 (개별) | | `PRODUCTION_COMPLETE` | 전체제작완료 | Order | 모든 항목 제작 완료 (발송 대기) | | `SHIPPED` | 발송완료 | Order·Item | 택배 발송 | | `DELIVERED` | 배송완료 | Order | 고객 수령 확인 | | `CANCELLED` | 취소 | Order·Item | 취소 (환불 없음) | | `CANCELLED_REFUND` | 취소환불 | Order | 취소 + 충전금 환불 (부분 취소 귀결 포함) | | `ERROR` | 오류 | Order·Item | 오류 상태 | ## 상태 — Webhook 이벤트 매핑 각 주문 상태 전이 시 발송되는 웹훅 이벤트와 주요 필드입니다. | 상태 | Webhook Event | 주요 필드 | | --- | --- | --- | | `PAID` | `order.created` | `total_amount`, `item_count` | | `CONFIRMED` | `production.confirmed` | `print_day` | | `IN_PRODUCTION` | `production.started` | `order_uid`, `order_status` | | `PRODUCTION_COMPLETE` | `production.completed` | `order_uid`, `order_status` | | `SHIPPED` | `shipping.departed` | `tracking_number`, `tracking_carrier` | | `DELIVERED` | `shipping.delivered` | `order_uid`, `order_status` | | `CANCELLED` / `CANCELLED_REFUND` (전체 취소) | `order.cancelled` | `cancel_reason`, `refund_amount` | | 주문 항목 부분 취소 (`orderStatus` 변경 없음) | `order.item_cancelled` | `cancelled_item_uid`, `remaining_item_uids`, `full_cancel`, `refund_amount` | | 마지막 항목 부분 취소 → `CANCELLED_REFUND` 귀결 | `order.item_cancelled` + `order.cancelled` (호환 발송) | `full_cancel: true`, `refund_amount` | | `PAID` (복원) | `order.restored` | `restore_reason`, `credit_deducted` | ## Sandbox 환경 동작 Sandbox 환경에서는 주문이 `PAID` 상태에서 멈춥니다. 실제 제작이나 배송이 진행되지 않으며, 상태 전이 테스트는 웹훅 테스트 기능을 활용하세요. > ⚠️ Sandbox에서는 `PAID` 이후 자동 상태 전이가 발생하지 않습니다. > 웹훅 수신 테스트는 `POST /webhooks/test`를 활용하세요. ## 취소 규칙 파트너가 API로 직접 취소할 수 있는 상태는 `PAID`와 `PDF_READY`뿐입니다. 이후 상태에서는 관리자 승인 절차를 통해서만 취소할 수 있습니다. ### 주문 전체 취소 - **엔드포인트**: `POST /orders/{orderUid}/cancel` - **취소 가능 상태**: `PAID`, `PDF_READY` - **결과 상태**: `CANCELLED_REFUND` - **충전금 환불**: 결제 금액 전액(배송비/포장비 포함) 즉시 환불 ### 주문 항목 부분 취소 - **엔드포인트**: `POST /orders/{orderUid}/items/{itemUid}/cancel` - **취소 가능 상태**: 주문·항목 모두 `PAID` / `PDF_READY` - **결과 상태**: 항목 `CANCELLED_REFUND` / 주문 기존 상태 유지 - **충전금 환불**: 남은 항목으로 재계산 → 차액 즉시 환불 ### 부분 취소 누적 → 마지막 항목 취소 - **엔드포인트**: `POST /orders/{orderUid}/items/{itemUid}/cancel` (마지막 남은 항목 1개에 대해 호출) - **취소 가능 상태**: 남은 항목이 1개뿐인 상태 - **결과 상태**: 항목·주문 모두 `CANCELLED_REFUND`, 응답에 `fullCancel: true` - **충전금 환불**: 배송비/포장비 포함 잔액 환불 부분 취소 상세 규약·요청/응답 포맷은 [Orders API](/docs/api/orders)의 **주문 항목 부분 취소** 섹션을 참조하세요. ## 제작 및 배송 SLA | 단계 | 소요 기간 | 비고 | | --- | --- | --- | | 제작 (CONFIRMED → PRODUCTION_COMPLETE) | 3~4 영업일 | 공휴일 제외 | | 배송 (SHIPPED → DELIVERED) | 1~2일 | 한진택배 | ## 관련 문서 - [Orders API — — 주문 생성 및 관리](/docs/api/orders) - [Webhook Events — — 이벤트별 페이로드 상세](/docs/api/webhook-events) - [충전금 운영 절차 — — 충전금 차감/환불 규칙](/docs/operations/credits) - [에러 코드 & 트러블슈팅 — — 주문 관련 문제 해결](/docs/operations/errors) --- # 충전금 모델 Book Print API의 충전금 결제 모델을 안내합니다. 환경 분리, 차감·환불 규칙, 거래 사유 코드, 잔액 부족 처리 등 충전금 도메인의 핵심 개념을 다룹니다. > ℹ️ 엔드포인트 호출 방법은 [Credits API](/docs/api/credits)를, 운영 절차(충전·모니터링·402 처리 패턴)는 [충전금 운영 절차](/docs/operations/credits)를 참고하세요. ## 모델 개요 SweetBook은 **선불 충전금 방식**으로 도서 제작 비용을 결제합니다. 파트너 포털에서 충전금을 선충전(Sandbox는 API 충전도 가능)한 뒤, 주문 생성 시 충전금이 자동으로 차감됩니다. 주문 취소 시에는 차감된 금액이 즉시 환불됩니다. - **차감 시점**: 주문 생성(`POST /orders`) 직후 즉시 - **환불 시점**: 주문 취소 직후 즉시 (전체 취소 `POST /orders/{orderUid}/cancel` · 부분 취소 `POST /orders/{orderUid}/items/{itemUid}/cancel` 둘 다 해당). 자세한 동작은 [Orders API](/docs/api/orders) 참고 - **통화**: KRW (원). `currency` 필드는 항상 `"KRW"` - **환경 분리**: Sandbox와 Live 충전금은 독립된 잔액으로 관리됩니다 (아래 **환경 분리** 섹션 참조) ## 환경 분리 Sandbox 환경과 Live 환경의 충전금은 **완전히 분리**되어 있습니다. 잔액이 어느 환경의 것인지는 사용한 API Key에 의해 자동 결정되며, `GET /credits` 응답 본문의 `env` 필드(`"test"` 또는 `"live"`)로 식별할 수 있습니다. | 환경 | `env` 값 | 충전 방법 | 차감/환불 동작 | | --- | --- | --- | --- | | **Sandbox** | `"test"` | 파트너 포털에서 직접 입력 (가상 충전금) | 실제 비용 발생 없음. 테스트용 차감/환불 | | **Live** | `"live"` | 파트너 포털 PG 결제 | 실제 결제·환불 처리 | > ⚠️ Sandbox 잔액과 Live 잔액은 서로 영향을 주지 않습니다. Sandbox에서 사용한 충전금이 Live 잔액을 차감하거나, 그 반대 경우도 발생하지 않습니다. ## 차감·환불 규칙 ### 차감 공식 주문 생성 시 차감되는 충전금(`paidCreditAmount`)은 다음 규칙으로 계산됩니다. 1. 주문 총액 계산: `totalAmount = productAmount + shippingFee + packagingFee` 2. 주문 총액에 **부가세 10%를 가산**: `totalAmount × 1.1` 3. **10원 단위로 절삭**(10원 미만은 버림): 최종 `paidCreditAmount` 예시(Sandbox 실 응답): `productAmount` 200원 + `shippingFee` 3,000원 + `packagingFee` 0원 = `totalAmount` 3,200원. 부가세 10% 가산 후 3,520원 → 10원 단위 절삭으로 **`paidCreditAmount` 3,520원**이 차감됩니다 (이 케이스는 결과가 이미 10의 배수라 절삭이 적용되지 않음). `productAmount`·`shippingFee`·`packagingFee`·`totalAmount`·`paidCreditAmount` 모두 `POST /orders`·`POST /orders/estimate` 응답에 포함됩니다. ### 환불 규칙 주문 취소 시 차감된 충전금이 즉시 환불됩니다. 환불 가능 여부는 주문 상태에 따라 결정됩니다. | 주문 상태 | 파트너 취소 가능 | 환불 동작 | | --- | --- | --- | | `PAID` | O | 전액 환불 (즉시) | | `PDF_READY` | O | 전액 환불 (즉시) | | `CONFIRMED` 이후 | X | 파트너가 직접 취소 불가 (제작 확정 이후 단계 — 부분 취소도 동일 조건) | 전체 취소 외에 **주문 항목별 부분 취소**도 지원됩니다. 환불액은 **남은 항목 기준으로 재계산한 차액**이며(포장비·배송비·부가세 재배분 포함), 단순 항목 차감액과 다를 수 있습니다. 상세 동작과 응답 예시는 [Orders API](/docs/api/orders)의 부분 취소 섹션을 참고하세요. **차감 공식** ```text paidCreditAmount = floor((productAmount + shippingFee + packagingFee) × 1.1 / 10) × 10 ``` ## 거래 사유 코드 (reasonCode) `GET /credits/transactions` 응답의 각 거래 항목에는 `reasonCode`(거래 사유 코드)와 `reasonDisplay`(한글 사유)가 포함됩니다. `reasonCode`는 거래 발생 시점·종류를 식별할 때 사용하고, `reasonDisplay`는 사용자 표시용 한글 라벨입니다. | 코드 | 의미 | 발생 시점 | 방향 | | --- | --- | --- | --- | | `1` | CREDIT_RECHARGE | 결제 완료 후 충전 (Live PG 결제) | + | | `3` | ORDER_PAYMENT | 주문 생성 시 결제 차감 | - | | `4` | ORDER_REFUND | 주문 환불 (일반) | + | | `7` | ORDER_CANCEL_REFUND | 파트너 전체 취소 환불 | + | | `8` | ADMIN_CANCEL_REFUND | 관리자 전체 취소 환불 | + | | `9` | SANDBOX_CHARGE | Sandbox 테스트 충전 (`POST /credits/sandbox/charge`) | + | | `10` | SANDBOX_DEDUCT | Sandbox 테스트 차감 (`POST /credits/sandbox/deduct`) | - | | `11` | ORDER_RESTORE_DEDUCT | 주문 복원 시 재차감 | - | | `13` | ORDER_MODIFY_REFUND | 주문 항목 부분 취소 환불 | + | 클라이언트가 사용자에게 거래 사유를 표시할 때는 `reasonDisplay` 한글 라벨을 그대로 사용하는 것을 권장합니다. `reasonCode`는 거래 분류·필터링 용도로 사용하세요. ## 잔액 부족 (402 Payment Required) 충전금 잔액이 차감 예정 금액(`paidCreditAmount`)보다 적은 상태에서 주문을 생성하면 `402 Payment Required` 응답이 반환되며, 주문은 생성되지 않습니다. ### 발생 조건 - `balance`(현재 잔액) **<** `required`(필요 금액 = 차감 예정 `paidCreditAmount`) - `errorCode`: `ERR_INSUFFICIENT_CREDIT` ### 응답 본문 `data` 객체에 진단 정보가 포함되어, 클라이언트가 부족 금액을 계산해 사용자에게 안내할 수 있습니다. - `required`: 결제에 필요한 금액 (원) - `balance`: 현재 잔액 (원) - `currency`: 통화 (항상 `"KRW"`) 잔액 부족 시 클라이언트 측 처리 패턴(사전 잔액 확인 + 충전 안내 + 재시도)은 [충전금 운영 절차](/docs/operations/credits)의 **402 에러 처리 패턴** 섹션을 참고하세요. errorCode 분기와 공통 에러 응답 구조는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참고하세요. **402 응답 예시** ```json { "success": false, "errorCode": "ERR_INSUFFICIENT_CREDIT", "message": "Insufficient Credit", "data": { "required": 14300, "balance": 3220.00, "currency": "KRW" }, "errors": ["잔액이 부족합니다. 필요: 14300, 잔액: 3220.00"], "fieldErrors": [] } ``` ## 관련 문서 - [Credits API — — 충전금 잔액·거래 내역 조회 엔드포인트](/docs/api/credits) - [충전금 운영 절차 — — 충전 절차·모니터링·402 처리 패턴](/docs/operations/credits) - [Orders API — — 주문 생성·취소·부분취소 (차감·환불 트리거)](/docs/api/orders) - [에러 코드 & 트러블슈팅 — — ERR_INSUFFICIENT_CREDIT 등 errorCode 분기](/docs/operations/errors) --- # Credits API 충전금 잔액·거래 내역 조회와 Sandbox 테스트 API의 엔드포인트 명세를 안내합니다. > ℹ️ 충전금 결제 모델·차감/환불 규칙·거래 사유 코드는 [충전금 모델](/docs/concepts/credit-model)을, 운영 절차는 [충전금 운영 절차](/docs/operations/credits)를 참고하세요. > ℹ️ 모든 Credits API는 인증이 필요합니다. API Key를 `Authorization: Bearer ` 헤더로 제공하세요. ## 충전금 잔액 조회 GET `/credits` 현재 파트너 계정의 충전금 잔액을 조회합니다. ### Request 예시 **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/credits' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `accountUid` | string | 계정 고유 UID | | `balance` | number | 현재 충전금 잔액 (원) | | `currency` | string | 통화 코드 (항상 `"KRW"`) | | `env` | string | 환경 (`"test"` 또는 `"live"`) | | `createdAt` | datetime | 계정 생성 일시 | | `updatedAt` | datetime | 잔액 마지막 변경 일시 | > ⚠️ **환경 분리:** Sandbox/Live 충전금은 분리되어 있으며, API Key의 환경에 해당하는 잔액만 반환됩니다. [충전금 모델 — 환경 분리](/docs/concepts/credit-model#env-isolation) 참조. ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "성공", "data": { "accountUid": "u_6b0f********************", "balance": 40370.00, "currency": "KRW", "createdAt": "2026-03-13T12:16:15.000Z", "updatedAt": "2026-04-29T02:20:01.000Z", "env": "test" } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## 충전금 거래 내역 조회 GET `/credits/transactions` 본인 계정의 충전금 거래 내역(충전, 차감, 환불 등)을 조회합니다. 거래는 **최근 발생 순(`createdAt` 내림차순)**으로 반환됩니다. > ⚠️ **환경 분리:** Sandbox/Live 충전금은 분리되어 있으며, API Key의 환경에 해당하는 거래 내역만 반환됩니다. [충전금 모델 — 환경 분리](/docs/concepts/credit-model#env-isolation) 참조. ### Request 예시 **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/credits/transactions' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `transactionId` | number | 거래 고유 ID | | `accountUid` | string | 계정 고유 UID | | `reasonCode` | number | 거래 사유 코드 | | `reasonDisplay` | string | 거래 사유 설명 (사람이 읽을 수 있는 형식) | | `direction` | string | 거래 방향 (`+` 증액, `-` 차감) | | `currency` | string | 통화 (예: `KRW`) | | `amount` | number | 거래 금액 (원, 부호 포함 — 차감 시 음수) | | `balanceAfter` | number | 거래 후 잔액 (원) | | `memo` | string | 거래 메모 (없으면 `null`) | | `createdAt` | datetime | 거래 일시 (ISO 8601, UTC) | | `isTest` | boolean | 테스트 환경 거래 여부 | 응답 각 거래 항목에는 `reasonCode`(거래 사유 코드)와 `reasonDisplay`(한글 라벨)가 포함됩니다. 코드별 의미와 발생 시점은 [충전금 모델 — 거래 사유 코드](/docs/concepts/credit-model#reason-code)를 참고하세요. ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "성공", "data": [ { "transactionId": 166, "accountUid": "u_6b0f********************", "reasonCode": 10, "reasonDisplay": "샌드박스 차감", "direction": "-", "currency": "KRW", "amount": -100.00, "balanceAfter": 40370.00, "memo": "test deduct", "createdAt": "2026-04-29T02:20:01.000Z", "isTest": true }, { "transactionId": 165, "accountUid": "u_6b0f********************", "reasonCode": 9, "reasonDisplay": "샌드박스 충전", "direction": "+", "currency": "KRW", "amount": 1000.00, "balanceAfter": 40470.00, "memo": "test charge", "createdAt": "2026-04-29T02:17:33.000Z", "isTest": true }, { "transactionId": 163, "accountUid": "u_6b0f********************", "reasonCode": 3, "reasonDisplay": "주문 결제 차감", "direction": "-", "currency": "KRW", "amount": -3520.00, "balanceAfter": 39360.00, "memo": "주문 결제: or_403T********", "createdAt": "2026-04-27T06:15:11.000Z", "isTest": true } ], "pagination": { "total": 166, "limit": 20, "offset": 0, "hasNext": true } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회 성공 (거래 없음 시 빈 배열) | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## Sandbox 충전금 충전 (테스트용) POST `/credits/sandbox/charge` 테스트 목적으로 충전금을 충전합니다. **항상 test 충전금에 적용됩니다 (API Key 환경과 무관)**. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `amount` | number | Y | 충전할 금액 (원, 양수) | | `memo` | string | N | 충전 메모 (최대 200자) | > ⚠️ **memo 인코딩:** 한글 memo는 요청 본문이 UTF-8로 정확히 인코딩되도록 `Content-Type: application/json; charset=utf-8` 헤더를 명시하세요. 인코딩이 맞지 않으면 ModelBinding 단계에서 `400 ERR_VALIDATION_FAILED`가 반환됩니다. ### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/credits/sandbox/charge' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json; charset=utf-8' \\ -d '{ "amount": 1000, "memo": "test charge" }' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `accountUid` | string | 계정 고유 UID | | `balance` | number | 충전 후 잔액 (원) | | `currency` | string | 통화 코드 (항상 `"KRW"`) | | `env` | string | 환경 (`"test"` 고정) | | `createdAt` | datetime | 계정 생성 일시 | | `updatedAt` | datetime | 잔액 갱신 일시 | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "샌드박스 크레딧 충전 성공", "data": { "accountUid": "u_6b0f********************", "balance": 40470.00, "currency": "KRW", "createdAt": "2026-03-13T12:16:15.000Z", "updatedAt": "2026-04-29T02:17:33.497Z", "env": "test" } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 충전 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `amount` 누락·음수·0, `memo` 인코딩 오류 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## Sandbox 충전금 차감 (테스트용) POST `/credits/sandbox/deduct` 테스트 목적으로 충전금을 차감합니다. 잔액 부족 시나리오 등을 테스트할 때 사용합니다. **항상 test 충전금에 적용됩니다 (API Key 환경과 무관)**. ### Request Body | 필드 | 타입 | 필수 | 설명 | | --- | --- | --- | --- | | `amount` | number | Y | 차감할 금액 (원, 양수) | | `memo` | string | N | 차감 메모 (최대 200자) | > ⚠️ **memo 인코딩:** 한글 memo는 요청 본문이 UTF-8로 정확히 인코딩되도록 `Content-Type: application/json; charset=utf-8` 헤더를 명시하세요. 인코딩이 맞지 않으면 ModelBinding 단계에서 `400 ERR_VALIDATION_FAILED`가 반환됩니다. ### Request 예시 **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/credits/sandbox/deduct' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json; charset=utf-8' \\ -d '{ "amount": 100, "memo": "test deduct" }' ``` ### 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `accountUid` | string | 계정 고유 UID | | `balance` | number | 차감 후 잔액 (원) | | `currency` | string | 통화 코드 (항상 `"KRW"`) | | `env` | string | 환경 (`"test"` 고정) | | `createdAt` | datetime | 계정 생성 일시 | | `updatedAt` | datetime | 잔액 갱신 일시 | ### Response 예시 **Response (200 OK)** ```json { "success": true, "message": "샌드박스 크레딧 차감 성공", "data": { "accountUid": "u_6b0f********************", "balance": 40370.00, "currency": "KRW", "createdAt": "2026-03-13T12:16:15.000Z", "updatedAt": "2026-04-29T02:20:01.238Z", "env": "test" } } ``` ### HTTP 상태 코드 | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 차감 성공 | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `amount` 누락·음수·0, `memo` 인코딩 오류 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 402 Payment Required | `ERR_INSUFFICIENT_CREDIT` | 잔액 부족 — 차감 가능 금액보다 큰 값 요청 시 | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | ## 관련 문서 - [충전금 모델 — — 환경 분리·차감/환불 규칙·거래 사유 코드·402 잔액 부족](/docs/concepts/credit-model) - [충전금 운영 절차 — — 충전 절차·모니터링·402 처리 패턴](/docs/operations/credits) - [Orders API — — 주문 생성 및 취소 (차감·환불 트리거)](/docs/api/orders) - [에러 코드 & 트러블슈팅 — — HTTP 상태 코드 및 에러 응답](/docs/operations/errors) --- # 충전금 운영 절차 충전 절차, 잔액 모니터링, 402 처리 패턴 등 운영 단계의 절차를 안내합니다. > ℹ️ 충전금 결제 모델·차감/환불 규칙·거래 사유 코드는 [충전금 모델](/docs/concepts/credit-model)을, 엔드포인트 명세는 [Credits API](/docs/api/credits)를 참고하세요. ## 충전 절차 Sandbox와 Live 환경의 충전 방법은 다음과 같습니다. | 환경 | 충전 방법 | 비고 | | --- | --- | --- | | **Sandbox** | **파트너 포털 > 충전금 > 충전** 메뉴에서 직접 입력 (가상 충전금) | 실제 비용 발생 없음. 원하는 금액만큼 충전 가능 | | **Live** | **파트너 포털 > 충전금 > 충전** 메뉴에서 PG 결제 | 실제 결제·환불 처리 | > ℹ️ **Sandbox API 충전:** 통합 테스트 자동화 등 코드 흐름에서 충전이 필요한 경우 `POST /credits/sandbox/charge` API로도 충전할 수 있습니다 (Sandbox 전용). 상세는 [Credits API](/docs/api/credits)를 참고하세요. > ⚠️ **환경 분리:** Sandbox와 Live 환경의 충전금은 완전히 분리되어 있습니다. Sandbox에서 사용한 충전금은 Live에 영향을 주지 않습니다. 상세는 [충전금 모델 — 환경 분리](/docs/concepts/credit-model#env-isolation)를 참고하세요. ## 차감/환불 동작 충전금은 주문 생성 시 **자동 차감**되고 주문 취소 시 **자동 환불**됩니다. 별도 충전금 차감/환불 API 호출이 필요하지 않습니다. 차감 공식·환불 가능 상태·부분 취소 환불 규칙 등 상세 모델은 [충전금 모델 — 차감·환불 규칙](/docs/concepts/credit-model#deduct-refund)을 참고하세요. ## 모니터링 권장사항 - **잔액 부족 알림 설정:** `GET /credits`로 잔액을 정기 조회해 일정 기준 이하로 떨어지면 클라이언트가 알림을 발송하도록 설정하세요. 기준값은 평균 주문 금액·일 주문 빈도에 따라 결정하세요. - **거래 내역 모니터링:** `GET /credits/transactions`로 정기적으로 거래 내역을 확인하세요. 정렬은 최근 거래 순(`createdAt` 내림차순)이며, 운영 회계와 대조 시 응답의 `balanceAfter` 필드로 시점별 잔액을 확인할 수 있습니다. 주문 직전 잔액 검증은 `POST /orders/estimate`로 한 번에 처리하는 것을 권장합니다. 아래 **402 에러 처리 패턴** 참조. ## 402 에러 처리 패턴 충전금이 부족한 상태에서 주문을 생성하면 `402 Payment Required`(`ERR_INSUFFICIENT_CREDIT`)가 반환됩니다. 다음 패턴으로 처리하는 것을 권장합니다. - **견적으로 사전 검증:** `POST /orders/estimate` 응답의 `data` 객체에 `paidCreditAmount`·`creditBalance`·`creditSufficient`가 포함되어, 한 번의 호출로 차감 예정액과 충분 여부를 확인할 수 있습니다. - `creditSufficient: false`면 클라이언트가 충전 안내를 처리 (자동 충전 권장 안 함 — 의도하지 않은 결제 발생 방지) - 충전 후 동일 요청 본문으로 재시도 (`Idempotency-Key`를 활용해 중복 주문 방지) - **동시성 대비:** 견적 통과 후에도 다른 클라이언트의 동시 차감으로 `POST /orders`가 402를 반환할 수 있으므로 분기를 함께 처리합니다. 402 응답 본문 shape(`data`에 `required`·`balance`·`currency` 동봉) 등 errorCode 분기는 [에러 코드 & 트러블슈팅](/docs/operations/errors)의 **케이스 E**를 참고하세요. **Node.js** ```javascript async function createOrderWithEstimate(orderData, idempotencyKey) { const baseUrl = 'https://api-sandbox.sweetbook.com'; const headers = { 'Authorization': 'Bearer {YOUR_API_KEY}', 'Content-Type': 'application/json; charset=utf-8', }; // 1. 견적으로 잔액 사전 검증 (paidCreditAmount + creditBalance + creditSufficient) const est = await fetch(\`\${baseUrl}/v1/orders/estimate\`, { method: 'POST', headers, body: JSON.stringify(orderData), }).then(res => res.json()); if (!est.data.creditSufficient) { // 충전 안내 (자동 충전 금지 — 의도하지 않은 결제 방지) throw new Error( \`충전금이 부족합니다. 필요: \${est.data.paidCreditAmount}, 잔액: \${est.data.creditBalance}\` ); } // 2. 주문 생성 (Idempotency-Key로 중복 방지) const response = await fetch(\`\${baseUrl}/v1/orders\`, { method: 'POST', headers: { ...headers, 'Idempotency-Key': idempotencyKey }, body: JSON.stringify(orderData), }); // 3. 동시성 대비: 견적 통과 후에도 동시 차감으로 402 가능 if (response.status === 402) { const body = await response.json(); throw new Error( \`충전금이 부족합니다. 필요: \${body.data.required}, 잔액: \${body.data.balance}\` ); } // 다른 에러(401/403/400 등)는 별도 처리 — /docs/operations/errors 참조 return response.json(); } ``` ## 관련 문서 - [충전금 모델 — — 환경 분리·차감/환불 규칙·거래 사유 코드·402 잔액 부족](/docs/concepts/credit-model) - [Credits API — — 충전금 잔액·거래 내역 조회 엔드포인트 명세](/docs/api/credits) - [Orders API — — 주문 생성·취소 (차감·환불 트리거)](/docs/api/orders) - [에러 코드 & 트러블슈팅 — — ERR_INSUFFICIENT_CREDIT 등 errorCode 분기](/docs/operations/errors) --- # Webhooks API 웹훅을 설정하여 주문 상태 변경 등의 이벤트를 실시간으로 수신하는 방법을 안내합니다. ## 웹훅이란? 웹훅은 특정 이벤트 발생 시 등록된 URL로 HTTP POST 요청을 보내는 방식입니다. 주문 상태를 주기적으로 폴링하지 않아도 실시간으로 변경 사항을 수신할 수 있습니다. 1. 이벤트 발생 (주문 결제 완료) 2. SweetBook 서버가 등록된 URL로 HTTP POST 전송 3. 수신 서버가 200 OK 응답 4. 전송 완료 (SUCCESS) > ℹ️ **Sandbox 환경:** 웹훅 이벤트는 Live와 Sandbox 환경 모두에서 발생합니다. > 페이로드의 `isTest` 필드로 Sandbox 이벤트 여부를 구분할 수 있습니다. ## 이벤트 종류 | 이벤트 | 발생 시점 | 활용 예시 | | --- | --- | --- | | `order.created` | 주문 생성 (충전금 차감 완료) | 주문 접수 알림, 내부 시스템 동기화 | | `order.cancelled` | 주문 취소 | 환불 처리, 재고 복원 | | `order.restored` | 주문 복원 (취소 철회) | 복원된 주문 재처리 | | `order.item_cancelled` | 주문 항목 부분 취소 | 항목 단위 환불 처리, 남은 항목 기반 재처리 | | `production.confirmed` | 제작 확정 | 제작 시작 안내 | | `production.started` | 제작 시작 | 제작 진행 상태 업데이트 | | `production.completed` | 제작 완료 | 배송 준비 안내 | | `shipping.departed` | 발송 완료 | 배송 추적 시작, 고객 알림 | | `shipping.delivered` | 배송 완료 | 배송 완료 안내, 리뷰 요청 | > ℹ️ 각 이벤트의 페이로드 구조와 JSON 예시는 [Webhook Events 레퍼런스](/docs/api/webhook-events)를 참고하세요. ## 웹훅 설정하기 ### 1. 웹훅 등록 `PUT /webhooks/config`로 수신 URL을 등록합니다. 최초 등록 시 `secretKey`가 자동 생성됩니다. | 필드 | 필수 | 설명 | | --- | --- | --- | | `webhookUrl` | Y | 수신 URL (HTTPS만 허용, 최대 500자) | | `events` | N | 구독할 이벤트 목록. `null`이면 전체 이벤트 구독 | | `description` | N | 설명 (최대 200자) | > ⚠️ **secretKey는 최초 등록 시에만 전체 값이 반환됩니다.** 이후 조회 시에는 앞 8자만 노출됩니다 (예: `whsk_a1b...`). > 발급 즉시 안전한 곳에 저장하세요. **Request** ```bash curl -X PUT 'https://api-sandbox.sweetbook.com/v1/webhooks/config' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "webhookUrl": "https://example.com/webhooks/sweetbook", "events": ["order.created", "shipping.departed"], "description": "주문 알림 수신" }' ``` ### 2. 설정 조회/수정 `GET /webhooks/config`로 현재 설정을 확인할 수 있습니다. 설정을 변경하려면 `PUT /webhooks/config`를 다시 호출하면 됩니다. 기존 `secretKey`는 유지되며, 수정 응답에는 전체 `secretKey`가 포함됩니다. ### 3. 웹훅 해제 `DELETE /webhooks/config`로 웹훅을 비활성화합니다. 기존 전송 이력은 유지됩니다. ## 전송 헤더 웹훅 요청에는 다음 4개의 헤더가 포함됩니다. | 헤더 | 설명 | 예시 | | --- | --- | --- | | `X-Webhook-Signature` | HMAC-SHA256 서명값 (sha256= 접두사 포함) | `sha256=a1b2c3d4...` | | `X-Webhook-Timestamp` | 서명 생성 시점 (Unix timestamp, 초) | `1709280000` | | `X-Webhook-Event` | 이벤트 타입 | `order.created` | | `X-Webhook-Delivery` | 전송 고유 ID | `wh_abc123xyz` | ## 서명 검증 (HMAC-SHA256) 수신한 웹훅 요청이 SweetBook에서 보낸 것인지 확인하려면 서명을 검증해야 합니다. 서명은 `{timestamp}.{payload}` 형식의 문자열을 `secretKey`로 HMAC-SHA256 해시한 값입니다. **Signature Formula** ```text 서명 페이로드 = "{timestamp}.{JSON body}" 기대값 = "sha256=" + HMAC-SHA256(secretKey, 서명 페이로드) 검증 = 기대값 == X-Webhook-Signature 헤더값 (sha256={hex} 형식) ``` ### JavaScript (Express.js) **Node.js** ```javascript const crypto = require('crypto'); function verifySignature(payload, signature, timestamp, secretKey) { const signPayload = \`\${timestamp}.\${payload}\`; const expectedHex = crypto .createHmac('sha256', secretKey) .update(signPayload) .digest('hex'); const expected = \`sha256=\${expectedHex}\`; return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } app.post('/webhooks/sweetbook', express.json(), (req, res) => { const signature = req.headers['x-webhook-signature']; const timestamp = req.headers['x-webhook-timestamp']; const event = req.headers['x-webhook-event']; const payload = JSON.stringify(req.body); if (!verifySignature(payload, signature, timestamp, SECRET_KEY)) { return res.status(401).json({ error: 'Invalid signature' }); } // 이벤트 처리 switch (event) { case 'order.created': console.log('주문 생성 완료:', req.body); break; case 'shipping.departed': console.log('발송 완료:', req.body); break; } res.status(200).json({ received: true }); }); ``` ### Python (Flask) > ℹ️ 서명 비교 시 반드시 타이밍 공격을 방지하는 함수를 사용하세요. > JavaScript: `crypto.timingSafeEqual()`, Python: `hmac.compare_digest()` **Python** ```python import hmac import hashlib from flask import Flask, request, jsonify app = Flask(__name__) SECRET_KEY = "whsk_your_secret_key" def verify_signature(payload, signature, timestamp, secret_key): sign_payload = f"{timestamp}.{payload}" expected_hex = hmac.new( secret_key.encode(), sign_payload.encode(), hashlib.sha256 ).hexdigest() expected = f"sha256={expected_hex}" return hmac.compare_digest(signature, expected) @app.route('/webhooks/sweetbook', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') timestamp = request.headers.get('X-Webhook-Timestamp') event = request.headers.get('X-Webhook-Event') payload = request.get_data(as_text=True) if not verify_signature(payload, signature, timestamp, SECRET_KEY): return jsonify({"error": "Invalid signature"}), 401 # 이벤트 처리 data = request.get_json() if event == 'order.created': print(f"주문 생성 완료: {data}") elif event == 'shipping.departed': print(f"발송 완료: {data}") return jsonify({"received": True}), 200 ``` ## 테스트하기 `POST /webhooks/test`로 테스트 이벤트를 전송하여 수신 서버가 정상 동작하는지 확인할 수 있습니다. 샘플 데이터가 전송되며, 실제 주문 데이터는 포함되지 않습니다. > ℹ️ 테스트 전송은 재시도되지 않습니다. 실패 시 응답에서 `responseStatus`와 `responseBody`를 확인하여 문제를 진단하세요. **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/webhooks/test' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{"eventType": "order.created"}' ``` **Response** ```json { "success": true, "message": "Success", "data": { "deliveryUid": "wh_abc123xyz", "eventType": "order.created", "status": "SUCCESS", "responseStatus": 200, "responseBody": "{\\"received\\": true}" } } ``` ## 재시도 정책 전송 실패(2xx 외 응답 또는 타임아웃) 시 자동으로 최대 3회 재시도합니다. **FAILED** 상태인 건만 재시도 대상이며, **PENDING** 상태는 재시도되지 않습니다. HTTP 타임아웃은 30초입니다. 테스트 전송(`isTest: true`)은 재시도 없이 1회만 전송됩니다. | 시도 | 대기 시간 | | --- | --- | | 1차 재시도 | 1분 후 | | 2차 재시도 | 5분 후 | | 3차 재시도 | 30분 후 | 3회 모두 실패하면 상태가 `EXHAUSTED`로 변경됩니다. > ⚠️ **EXHAUSTED 알림:** 모든 재시도가 소진되면 `webhook.exhausted` 이벤트가 트리거됩니다. > 동일 알림은 **1시간 쿨다운**이 적용되어 과도한 알림이 발생하지 않습니다. ### 전송 상태 | 상태 | 설명 | | --- | --- | | `PENDING` | 전송 대기 또는 진행 중 | | `SUCCESS` | 전송 성공 (2xx 응답) | | `FAILED` | 전송 실패 (재시도 대기 중) | | `EXHAUSTED` | 최대 재시도(3회) 초과 | ## 전송 이력 확인 `GET /webhooks/deliveries`로 전송 이력을 조회할 수 있습니다. `eventType`과 `status`로 필터링할 수 있습니다. **Request** ```bash # 실패한 전송 이력만 조회 curl 'https://api-sandbox.sweetbook.com/v1/webhooks/deliveries?status=FAILED&limit=10' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ## 수신 서버 구현 시 유의사항 - 수신 서버는 **200~299** 상태 코드로 응답해야 합니다. 그 외 응답은 실패로 처리됩니다. - **30초** 이내에 응답해야 합니다. 시간이 오래 걸리는 작업은 비동기로 처리하세요. - `X-Webhook-Signature`를 반드시 검증하여 위변조 요청을 차단하세요. - 같은 이벤트가 중복 전송될 수 있습니다. `X-Webhook-Delivery` 헤더로 중복 여부를 확인하세요. - 수신 URL은 **HTTPS**만 허용됩니다. ## HTTP 상태 코드 Webhooks API의 5개 엔드포인트(`PUT /webhooks/config`, `GET /webhooks/config`, `DELETE /webhooks/config`, `POST /webhooks/test`, `GET /webhooks/deliveries`)에서 반환되는 상태 코드입니다. | 코드 | errorCode | 설명 | | --- | --- | --- | | 200 OK | — | 조회/수정/삭제/테스트 성공 | | 201 Created | — | 웹훅 최초 등록 성공 (`PUT /webhooks/config`) | | 400 Bad Request | `ERR_VALIDATION_FAILED` | `webhookUrl` 형식 오류, HTTPS 아님, 미지원 `eventType` 등 | | 401 Unauthorized | `ERR_UNAUTHORIZED` | 인증 실패 | | 404 Not Found | `ERR_NOT_FOUND` | 등록된 웹훅 설정 없음 (`GET`/`DELETE`/`POST /webhooks/test`) | | 500 Internal Server Error | `ERR_INTERNAL_ERROR` | 서버 오류 | 실패 응답 6필드 shape(`success`·`errorCode`·`message`·`data`·`errors`·`fieldErrors`) 및 errorCode 분기 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참고하세요. ## 관련 문서 - [Webhook Events — — 9개 이벤트별 페이로드 구조와 예시](/docs/api/webhook-events) - [Orders API — — 이벤트 발생 트리거(주문 생성·취소·부분취소)](/docs/api/orders) - [주문 상태 흐름 — — 이벤트가 발생하는 상태 전이](/docs/operations/order-status) - [에러 코드 & 트러블슈팅 — — 웹훅 수신 실패 시 errorCode 분기·멱등 처리](/docs/operations/errors) --- # Webhook Events 레퍼런스 Book Print API에서 발생하는 9가지 웹훅 이벤트의 페이로드 구조와 예시를 안내합니다. ## 공통 페이로드 구조 모든 웹훅 이벤트는 아래의 공통 필드를 포함합니다. | 필드 | 타입 | 설명 | | --- | --- | --- | | `event_uid` | string | 이벤트 고유 ID | | `event_type` | string | 이벤트 타입 (예: `order.created`) | | `created_at` | string | 이벤트 발생 시각 (ISO 8601) | | `data` | object | 이벤트별 페이로드 데이터 | ## 전송 헤더 모든 웹훅 요청에는 다음 헤더가 포함됩니다. 서명 검증 시 반드시 확인하세요. | 헤더 | 설명 | 예시 | | --- | --- | --- | | `X-Webhook-Signature` | HMAC-SHA256 서명값 (sha256= 접두사 포함) | `sha256=e3b0c44298fc1c14...` | | `X-Webhook-Timestamp` | 서명 생성 시점 (Unix timestamp, 초) | `1709280000` | | `X-Webhook-Event` | 이벤트 타입 | `order.created` | | `X-Webhook-Delivery` | 전송 고유 ID (중복 수신 확인용) | `wh_d7e8f9a0b1c2` | ## 전송 상태 (Delivery Status) | 상태 | 설명 | | --- | --- | | `PENDING` | 전송 대기 또는 진행 중 | | `SUCCESS` | 전송 성공 (수신 서버가 2xx 응답) | | `FAILED` | 전송 실패 (재시도 대기 중, 최대 3회) | | `EXHAUSTED` | 최대 재시도 횟수(3회) 초과 — 수동 확인 필요 | ## order.created — 주문 생성 (충전금 차감 완료) 주문이 생성되고 충전금 차감이 완료되면 발생합니다. 내부 시스템에 주문 접수를 동기화하거나, 고객에게 주문 접수 알림을 보낼 때 활용합니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 주문 상태 | | `total_amount` | 주문 총액 | | `item_count` | 주문 항목 수 | | `ordered_at` | 주문 시각 | **order.created** ```json { "event_uid": "evt_a1b2c3d4", "event_type": "order.created", "created_at": "2025-03-15T10:30:00Z", "data": { "order_uid": "or_8f3a2b1c", "order_status": "PAID", "total_amount": 35000, "item_count": 2, "ordered_at": "2025-03-15T10:30:00Z" } } ``` ## order.cancelled — 주문 취소 주문이 취소되면 발생합니다. 취소 사유와 충전금 환불 정보가 포함됩니다. 환불 처리나 내부 재고 복원 로직에 활용합니다. 주문 항목 부분 취소(`POST /orders/{orderUid}/items/{itemUid}/cancel`)로 마지막 항목까지 취소되어 주문 전체가 취소되는 경우에도 호환 목적으로 `order.cancelled`가 함께 발송됩니다. 이때는 `order.item_cancelled`(`full_cancel: true`)도 같이 발송됩니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 주문 상태 | | `cancel_reason` | 취소 사유 | | `refund_amount` | 환불 금액 | | `cancelled_at` | 취소 시각 | **order.cancelled** ```json { "event_uid": "evt_b2c3d4e5", "event_type": "order.cancelled", "created_at": "2025-03-16T11:20:00Z", "data": { "order_uid": "or_8f3a2b1c", "order_status": "CANCELLED", "cancel_reason": "고객 요청에 의한 취소", "refund_amount": 35000, "cancelled_at": "2025-03-16T11:20:00Z" } } ``` ## order.item_cancelled — 주문 항목 부분 취소 주문에 포함된 개별 항목이 부분 취소(`POST /orders/{orderUid}/items/{itemUid}/cancel`)될 때 발생합니다. 환불 처리나 항목 단위 재고 복원 로직에 활용합니다. `full_cancel`이 `true`이면 이번 호출이 마지막 항목 취소로 이어져 주문 전체가 취소된 경우입니다. 이때는 호환 목적으로 `order.cancelled`도 함께 발송되므로, 두 이벤트를 모두 구독 중이라면 중복 처리되지 않도록 `event_uid` 기반으로 처리하세요. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 처리 후 주문 상태 (부분 취소 시 20/25 유지, 전체 취소 귀결 시 81) | | `full_cancel` | 이번 호출이 전체 취소로 귀결됐는지 여부 (`true`/`false`) | | `cancelled_item_uid` | 이번 호출로 취소된 항목 UID | | `remaining_item_uids` | 남은(살아있는) 항목 UID 배열 | | `cancel_reason` | 취소 사유 | | `refund_amount` | 이번 호출로 환불된 금액 (원, decimal) | | `cancelled_at` | 취소 시각 (ISO 8601, UTC) | **order.item_cancelled (부분 취소)** ```json { "event_uid": "evt_c3d4e5f6", "event_type": "order.item_cancelled", "created_at": "2026-04-22T01:23:00Z", "data": { "order_uid": "or_2lnj********", "order_status": "PAID", "full_cancel": false, "cancelled_item_uid": "oi_6bGM********", "remaining_item_uids": ["oi_1Nrc********"], "cancel_reason": "고객 요청 (1권 제외)", "refund_amount": 110.00, "cancelled_at": "2026-04-22T01:23:00Z" } } ``` **order.item_cancelled (전체 취소 귀결)** ```json { "event_uid": "evt_d4e5f6g7", "event_type": "order.item_cancelled", "created_at": "2026-04-22T01:21:38Z", "data": { "order_uid": "or_184U********", "order_status": "CANCELLED_REFUND", "full_cancel": true, "cancelled_item_uid": "oi_34Hu********", "remaining_item_uids": [], "cancel_reason": "Test full cancel via partial", "refund_amount": 3410.00, "cancelled_at": "2026-04-22T01:21:38Z" } } ``` ## order.restored — 주문 복원 취소된 주문이 복원(취소 철회)되면 발생합니다. 복원 시 충전금이 다시 차감됩니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 주문 상태 (문자열 enum) | | `restore_reason` | 복원 사유 | | `restored_at` | 복원 시각 | | `credit_deducted` | 복원 시 충전금 재차감 여부 (boolean) | **order.restored** ```json { "event_uid": "evt_c3d4e5f6", "event_type": "order.restored", "created_at": "2026-03-16T13:00:00Z", "data": { "order_uid": "or_8f3a********", "order_status": "PAID", "restore_reason": "파트너 요청으로 복원", "restored_at": "2026-03-16T13:00:00Z", "credit_deducted": true } } ``` 페이로드에는 SweetBook 관리자 내부 입력 필드(`restored_by`)가 함께 전송될 수 있으나, 파트너 측 처리에는 의미가 없으므로 무시하면 됩니다. ## production.confirmed — 제작 확정 주문이 검수를 통과하여 제작이 확정되면 발생합니다. 고객에게 제작 시작 안내를 보내거나, 예상 완료일을 표시할 때 활용합니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 주문 상태 | | `print_day` | 출력 예정일 | | `confirmed_at` | 제작 확정 시각 | **production.confirmed** ```json { "event_uid": "evt_d4e5f6g7", "event_type": "production.confirmed", "created_at": "2025-03-15T14:00:00Z", "data": { "order_uid": "or_8f3a2b1c", "order_status": "CONFIRMED", "print_day": "2025-03-20", "confirmed_at": "2025-03-15T14:00:00Z" } } ``` ## production.started — 제작 시작 인쇄 제작이 시작되면 발생합니다. 제작 진행 중임을 고객에게 안내할 때 활용합니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 주문 상태 | | `changed_at` | 상태 변경 시각 | **production.started** ```json { "event_uid": "evt_e5f6g7h8", "event_type": "production.started", "created_at": "2025-03-17T09:00:00Z", "data": { "order_uid": "or_8f3a2b1c", "order_status": "IN_PRODUCTION", "changed_at": "2025-03-17T09:00:00Z" } } ``` ## production.completed — 제작 완료 모든 제작이 완료되어 배송 준비 단계에 진입하면 발생합니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 주문 상태 | | `completed_at` | 제작 완료 시각 | **production.completed** ```json { "event_uid": "evt_f6g7h8i9", "event_type": "production.completed", "created_at": "2025-03-19T17:30:00Z", "data": { "order_uid": "or_8f3a2b1c", "order_status": "PRODUCTION_COMPLETE", "completed_at": "2025-03-19T17:30:00Z" } } ``` ## shipping.departed — 발송 완료 제작이 완료되어 택배사에 인계되면 발생합니다. 운송장 번호(`trackingNumber`)와 택배사 정보(`trackingCarrier`)가 포함되어 배송 추적을 시작하거나 고객에게 발송 알림을 보낼 때 활용합니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `item_uid` | 주문 항목 고유 ID | | `tracking_number` | 운송장 번호 | | `tracking_carrier` | 택배사 코드 (예: `CJ`, `HANJIN`, `LOTTE`) | | `shipped_at` | 발송 시각 | **shipping.departed** ```json { "event_uid": "evt_g7h8i9j0", "event_type": "shipping.departed", "created_at": "2025-03-20T16:45:00Z", "data": { "order_uid": "or_8f3a2b1c", "item_uid": "oi_x1y2z3", "tracking_number": "1234567890123", "tracking_carrier": "CJ", "shipped_at": "2025-03-20T16:45:00Z" } } ``` ## shipping.delivered — 배송 완료 배송이 완료되면 발생합니다. 배송 완료 안내 또는 리뷰 요청에 활용합니다. ### 주요 필드 (data) | 필드 | 설명 | | --- | --- | | `order_uid` | 주문 고유 ID | | `order_status` | 주문 상태 | | `changed_at` | 상태 변경 시각 | **shipping.delivered** ```json { "event_uid": "evt_h8i9j0k1", "event_type": "shipping.delivered", "created_at": "2025-03-22T14:00:00Z", "data": { "order_uid": "or_8f3a2b1c", "order_status": "DELIVERED", "changed_at": "2025-03-22T14:00:00Z" } } ``` ## 관련 문서 - [Webhooks API — — 웹훅 등록·서명 검증·재시도 정책](/docs/api/webhooks) - [Orders API — — 이벤트 발생 트리거(주문 생성·취소·부분취소)](/docs/api/orders) - [주문 상태 흐름 — — 이벤트가 트리거되는 상태 전이 매핑](/docs/operations/order-status) - [에러 코드 & 트러블슈팅 — — 이벤트 처리 실패 시 errorCode 분기](/docs/operations/errors) --- # API 공통 사항 Book Print API의 인증, 요청/응답 형식, 페이지네이션, Rate Limiting 등 공통 규칙을 안내합니다. ## 인증 모든 API 요청에는 **Bearer Token 인증**이 필요합니다. 파트너 포털에서 발급받은 API Key를 `Authorization` 헤더에 포함하세요. API Key는 `SB`로 시작하는 **12자 prefix**(`SB` + 10자 식별자)와 **secret**이 `.`으로 연결된 형식입니다. Sandbox 키와 Live 키가 분리되어 있으므로 환경에 맞는 키를 사용하세요. > ⚠️ **보안 주의:** API Key는 서버 측에서만 사용하세요. 클라이언트(브라우저, 앱)에 노출하지 마세요. **Authorization header** ```text Authorization: Bearer SB{10자 식별자}.{secret} 예시 (더미): Authorization: Bearer SBXXXXXXXXXX.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ## Base URL | 환경 | Base URL | 용도 | | --- | --- | --- | | **Sandbox** | `https://api-sandbox.sweetbook.com/v1` | 개발 및 테스트 | | **Live** | `https://api.sweetbook.com/v1` | 실제 운영 | > ℹ️ Sandbox 환경에서는 실제 인쇄/배송이 이루어지지 않으며, Sandbox 충전금이 사용됩니다. ## 요청 / 응답 형식 대부분의 API 요청은 JSON 형식을 사용합니다. 파일 업로드가 필요한 엔드포인트는 `multipart/form-data`를 사용합니다. | Content-Type | 대상 엔드포인트 | | --- | --- | | `application/json` | 일반 API (Orders, Webhooks 등) | | `multipart/form-data` | 파일 업로드 (Cover, Contents, Photos) | ### 응답 필드 성공/실패 모두 공통 shape을 사용합니다. 실패 응답은 **6필드 고정**(`success`·`errorCode`·`message`·`data`·`errors`·`fieldErrors`)이며 `errorCode`·`errors`·`fieldErrors`가 **항상 존재**합니다. `errorCode`는 HTTP 상태 기반 기본값이 자동 주입되어 빈 문자열이 되지 않으며, `errors`·`fieldErrors`는 해당 항목이 없으면 빈 배열(`[]`)로 반환됩니다. | 필드 | 타입 | 성공 | 실패 | 설명 | | --- | --- | --- | --- | --- | | `success` | boolean | O | O | 요청 성공 여부(성공 `true` / 실패 `false`) | | `errorCode` | string | - | O | 실패 분기용 기계 판독 식별자. `ERR_*` 형식. 실패 시 항상 존재 | | `message` | string | O | O | HTTP 상태의 영어 라벨(예: `"Success"`, `"Bad Request"`) | | `data` | object | null | O | O | 응답 데이터. 실패 시 `null`이 기본이나 일부 에러는 진단 객체 포함(예: 402 `{required, balance, currency}`) | | `errors` | string[] | - | O | 사용자 표시용 한글 메시지 배열. 실패 시 항상 존재(비어 있으면 `[]`) | | `fieldErrors` | object[] | - | O | 필드 단위 구조화 에러 배열. 실패 시 항상 존재(해당 없으면 `[]`) | > ℹ️ **클라이언트 분기 권장:** 실패 분기는 `errorCode`(+ 필요 시 `fieldErrors[].constraint`)로 하세요. 사용자 표시용 한글은 `errors[0]`을 사용하고, `message`는 영어 라벨이라 표시 용도가 아닙니다. **메시지 문자열을 파싱해 분기하는 방식은 금지**합니다(문구가 바뀔 수 있음). 전체 errorCode 카탈로그는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. ### 성공 응답 요청이 성공하면 `success: true`와 함께 `data` 필드에 결과가 포함됩니다. **Response** ```json // 성공 응답 { "success": true, "message": "Success", "data": { ... } } ``` ### 에러 응답 요청이 실패하면 `success: false`와 함께 6필드 고정 shape으로 반환됩니다. `errorCode`는 **항상 존재**하며, 단순 에러는 `errors` 메시지만, 필드 단위 검증 실패는 `fieldErrors`에 구조화 정보가 포함됩니다. 참고: 도메인 검증(예: 페이지 제약, finalize 전제조건)의 `errors`·`message`는 한글로 반환되지만, 요청 본문의 필수 필드가 아예 누락되는 케이스(모델 바인딩 자동 검증)는 영어 라벨(예: `"The PostalCode field is required."`)로 반환될 수 있습니다. 클라이언트 분기는 `errorCode`와 `fieldErrors[].constraint`로 하세요. **Error response — 404 (단순 에러)** ```json { "success": false, "errorCode": "ERR_NOT_FOUND", "message": "Not Found", "data": null, "errors": ["책을 찾을 수 없습니다: bk_xxx"], "fieldErrors": [] } ``` **Error response — 400 (필드 검증)** ```json { "success": false, "errorCode": "ERR_VALIDATION_FAILED", "message": "Bad Request", "data": null, "errors": [ "shipping.address1: The Address1 field is required.", "shipping.postalCode: The PostalCode field is required.", "shipping.recipientName: The RecipientName field is required.", "shipping.recipientPhone: The RecipientPhone field is required." ], "fieldErrors": [ { "field": "shipping.address1", "message": "The Address1 field is required.", "constraint": "required" }, { "field": "shipping.postalCode", "message": "The PostalCode field is required.", "constraint": "required" }, { "field": "shipping.recipientName", "message": "The RecipientName field is required.", "constraint": "required" }, { "field": "shipping.recipientPhone", "message": "The RecipientPhone field is required.", "constraint": "required" } ] } ``` ### `fieldErrors` 항목 구조 필드 단위 검증 실패는 `fieldErrors` 배열의 각 항목이 아래 필드를 가집니다. `field`·`message`는 항상 존재하며, 나머지 3개는 해당 맥락에서 값이 있을 때만 포함됩니다(없으면 키 자체가 생략). | 필드 | 타입 | 항상 포함 | 설명 | | --- | --- | --- | --- | | `field` | string | O | 에러가 발생한 필드명. 중첩 필드는 점 표기(`shipping.recipientPhone`) | | `message` | string | O | 해당 필드의 한글 설명 | | `currentValue` | any | - | 현재 전달된 값(표시 가능할 때만) | | `requiredValue` | any | - | 기대값 또는 허용 범위. 단일 값(예: `24`)일 수도 있고 객체(예: `{`}"min": 50, "increment": 2{`}`) 형태일 수도 있음 | | `constraint` | string | - | 제약 종류. 클라이언트 분기에 사용. 아래 enum 6종 | #### `constraint` enum | 값 | 의미 | | --- | --- | | `required` | 필수 필드 누락 | | `min` | 최솟값 미만(숫자) 또는 최소 개수 미만(배열/페이지) | | `max` | 최댓값 초과(숫자) 또는 최대 개수 초과(배열/페이지) | | `increment` | 증분 규칙 위반(예: 페이지 수가 지정된 증분 단위의 배수가 아님) | | `enum` | 허용 목록 밖의 값 | | `pattern` | 정규식 패턴 불일치(형식 오류) | **fieldErrors 예시 (5필드)** ```json { "field": "pageCount", "message": "최소 24 페이지가 필요합니다", "currentValue": 10, "requiredValue": 24, "constraint": "min" } ``` ## 페이지네이션 목록 조회 API는 `limit`/`offset` 기반 페이지네이션을 사용합니다. 응답 구조는 **`data.{리소스명}` 배열과 `data.pagination` 객체**가 분리된 형태입니다. 리소스명은 엔드포인트에 따라 다릅니다 (`books`, `orders`, `templates` 등). ### Query 파라미터 | 파라미터 | 기본값 | 최대값 | 설명 | | --- | --- | --- | --- | | `limit` | 20 | 100 | 한 페이지에 반환할 항목 수 | | `offset` | 0 | - | 건너뛸 항목 수 | ### 공통 응답 필드 | 필드 | 타입 | 설명 | | --- | --- | --- | | `data.{리소스}` | array | 항목 배열. 키 이름은 엔드포인트마다 다름 (`books`, `orders`, `templates` 등) | | `data.pagination.total` | number | 전체 항목 수 | | `data.pagination.limit` | number | 현재 요청의 limit | | `data.pagination.offset` | number | 현재 요청의 offset | | `data.pagination.hasNext` | boolean | 다음 페이지 존재 여부 | > ℹ️ **list envelope 통일 (2026-05):** 모든 list 엔드포인트는 `data` 자체가 배열로 반환되고, 페이지네이션 메타는 `pagination` 최상위 형제 필드로 노출됩니다. 이전 패턴(`data.books`·`data.transactions`·`data.templates`·`data.photos` 등 리소스 키 중첩)은 더 이상 사용되지 않습니다. 단건 GET·단건 액션·통계 리포트는 변경 없습니다. **GET /books — Response (list envelope)** ```json { "success": true, "message": "성공", "data": [ /* ... book 항목들 */ ], "pagination": { "total": 0, "limit": 10, "offset": 0, "hasNext": false } } ``` **GET /orders — Response (list envelope)** ```json { "success": true, "message": "성공", "data": [ /* ... order 항목들 */ ], "pagination": { "total": 42, "limit": 20, "offset": 0, "hasNext": true } } ``` **GET /templates — Response (list envelope)** ```json { "success": true, "message": "성공", "data": [ /* ... template 항목들 */ ], "pagination": { "total": 120, "limit": 100, "offset": 0, "hasNext": true } } ``` ## Rate Limiting | 정책 | 대상 | 제한 | 기준 | | --- | --- | --- | --- | | **auth** | 인증 엔드포인트 | 10 req/min | IP 기반 | | **general** | 일반 API | 300 req/min | API Key 기반 | | **upload** | 파일 업로드 / Contents | 200 req/min | API Key 기반 | Rate Limit을 초과하면 `429 Too Many Requests` 응답이 반환됩니다. 응답의 `Retry-After` 헤더(초 단위)를 확인한 뒤 재시도하세요. `errorCode`는 `ERR_TOO_MANY_REQUESTS`로 반환됩니다. **Response headers** ```text HTTP/1.1 429 Too Many Requests Retry-After: 60 Content-Type: application/json ``` **Error response (429)** ```json { "success": false, "errorCode": "ERR_TOO_MANY_REQUESTS", "message": "Too Many Requests", "data": null, "errors": ["요청 한도를 초과했습니다. Retry-After 헤더 값만큼 대기 후 재시도하세요."], "fieldErrors": [] } ``` ## 멱등성 (Idempotency) 네트워크 오류로 동일한 요청이 중복 실행되는 것을 방지하기 위해 `Idempotency-Key` 헤더를 지원합니다. **Books/Credits/Orders 주요 POST 엔드포인트**(POST /books, POST /orders, POST /credits/sandbox/charge 등)에서 사용을 강력히 권장합니다. 동일한 `Idempotency-Key` + **같은 요청 본문**으로 재시도하면 **최초 응답이 그대로 재사용**됩니다(중복 처리 방지). 동일한 키 + **다른 요청 본문**을 보내면 `422 Unprocessable Entity` + `errorCode: "ERR_IDEMPOTENCY_KEY_MISMATCH"`가 반환됩니다. 따라서 재시도 시에는 반드시 **원본과 동일한 본문**을 같은 키로 보내야 합니다. | 엔드포인트 | 설명 | | --- | --- | | `POST /books` | 책 중복 생성 방지 | | `POST /books/{bookUid}/cover` | 표지 추가 중복 요청 방지 | | `POST /books/{bookUid}/contents` | 내지 추가 중복 요청 방지 | | `POST /books/{bookUid}/finalization` | 최종화 중복 요청 방지 | | `POST /orders` | 충전금 이중 차감 방지 | | `POST /orders/{orderUid}/cancel` | 전체 취소 중복 요청 방지 (환불 이중 처리 방지) | | `POST /orders/{orderUid}/items/{itemUid}/cancel` | 부분 취소 중복 요청 방지 (환불 이중 처리 방지) | > ⚠️ **중요:** 특히 주문 생성(POST /orders)에는 반드시 Idempotency-Key를 포함하세요. 충전금 이중 차감을 방지합니다. **Request** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/orders' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: unique-request-id-12345' \\ -d '{ ... }' ``` ## 주요 HTTP 에러 코드 자주 만나는 HTTP 상태 코드와 기본 `errorCode` 대응입니다. 상태 코드별 상세 케이스·응답 예시·마이그레이션 가이드는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을 참조하세요. | 상태 코드 | 기본 `errorCode` | 원인 | 해결 방법 | | --- | --- | --- | --- | | `400 Bad Request` | `ERR_VALIDATION_FAILED` | 요청 파라미터 또는 바디 유효성 검증 실패 | `fieldErrors`를 확인하여 오류 필드 수정 | | `401 Unauthorized` | `ERR_UNAUTHORIZED` | API Key 누락 또는 유효하지 않음 | Authorization 헤더에 올바른 API Key 포함 | | `402 Payment Required` | `ERR_INSUFFICIENT_CREDIT` | 충전금 잔액 부족 | `data`의 `required`·`balance` 확인 후 충전금 충전 | | `403 Forbidden` | `ERR_FORBIDDEN` | 접근 권한 없음 | 파트너 포털에서 API Key 상태 및 IP 제한 설정 확인 | | `404 Not Found` | `ERR_NOT_FOUND` | 요청한 리소스가 존재하지 않거나 접근 권한 없음 | UID 및 파트너 소유권 확인 | | `409 Conflict` | `ERR_CONFLICT` | 리소스 상태 충돌(예: 이미 등록된 표지 PDF에 POST 재호출) | 리소스 현재 상태 확인 후 적절한 메서드 사용 | | `422 Unprocessable Entity` | `ERR_IDEMPOTENCY_KEY_MISMATCH` | 동일 Idempotency-Key로 **다른 요청 본문** 전송 | 동일 키는 원본과 같은 본문으로만 재시도. 본문이 다르면 새 키 사용 | | `429 Too Many Requests` | `ERR_TOO_MANY_REQUESTS` | Rate Limit 초과 | `Retry-After` 헤더 값(초) 대기 후 재시도 | | `500 Internal Server Error` | `ERR_INTERNAL_ERROR` | 서버 내부 오류 | 잠시 후 재시도, 지속 시 support@sweetbook.com 문의 | | `501 Not Implemented` | `ERR_SANDBOX_UNSUPPORTED` | Sandbox에서 의도적으로 차단된 엔드포인트 호출 (Live 전용) | Live 환경에서만 호출. 자동 재시도 화이트리스트에서 제외. 엔드포인트별 환경 호출 가능 여부는 [환경 매트릭스](/docs/operations/environment-matrix) 참조 | **PDF 다운로드(`GET /books/{bookUid}/pdf-cover`·`GET /books/{bookUid}/pdf-contents`)**는 책의 `creationType`에 따라 위 일반 에러 외에 5종 추가 errorCode(`ERR_PDF_NOT_UPLOADED` 404 / `ERR_PDF_NOT_GENERATED` 409 / `ERR_PDF_PENDING` 409 / `ERR_PDF_GENERATION_FAILED` 422 / `ERR_PDF_FILE_MISSING` 500)로 분기됩니다. 자세한 매트릭스는 [Books — PDF 기반](/docs/api/books-pdf) / [Books — 템플릿 기반](/docs/api/books-template) / [에러 코드 & 트러블슈팅](/docs/operations/errors#pdf-download-branching)의 PDF 다운로드 섹션을 참조하세요. ## 날짜 형식 모든 날짜/시간 필드는 **ISO 8601** 형식(UTC)을 사용합니다. 응답 필드에 따라 밀리초 단위까지 포함될 수 있습니다. | 형식 | 예시 | 사용 맥락 | | --- | --- | --- | | 초 단위 | `2026-04-22T07:16:39.000Z` | 대부분의 타임스탬프 (`createdAt`, `orderedAt`, `paidAt` 등) | | 밀리초 포함 | `2026-04-22T01:04:38.104Z` | 주문 생성 등 시각 정밀도가 필요한 필드 (`createdAt` 일부) | ## 관련 문서 - [인증 (API Key) — — API Key 발급 및 인증 상세](/docs/authentication) - [환경 (Sandbox / Live) — — 환경별 Base URL 및 전환 안내](/docs/environments) - [Rate Limiting — — 요청 빈도 제한 상세 안내](/docs/operations/rate-limiting) - [페이지네이션 — — 목록 조회 페이징 상세](/docs/operations/pagination) - [멱등성 (Idempotency) — — 멱등성 키 상세 동작 원리](/docs/operations/idempotency) - [에러 코드 & 트러블슈팅 — — HTTP 상태 코드 및 에러 응답](/docs/operations/errors) --- # 환경 매트릭스 Book Print API의 모든 엔드포인트가 Sandbox 도메인과 Live 도메인에서 어떻게 동작하는지를 정리한 단일 진실원입니다. 클라이언트 입장에서 "이 API를 Sandbox에서 호출할 수 있나?"와 "호출은 되는데, 어떤 데이터가 영향받나?"를 한눈에 판단할 수 있도록 구성되어 있습니다. ## 도메인 식별 | 환경 | 도메인 | | --- | --- | | **Sandbox** | `api-sandbox.sweetbook.com` | | **Live** | `api.sweetbook.com` | 서버가 요청 도메인으로 환경을 자동 판별합니다. ## 한눈에 보기 — 4가지 분류 서버의 모든 엔드포인트는 다음 4가지 동작 중 하나를 합니다. 각 엔드포인트의 분류는 아래 컨트롤러별 표의 **분류** 컬럼에서 확인할 수 있습니다. | 코드 | 이름 | Sandbox 호출 결과 | Live 호출 결과 | 클라가 알아야 할 것 | | --- | --- | --- | --- | --- | | **A** | **환경 분리** | ✅ 호출 가능 — Sandbox 데이터(`is_test=true`)만 처리 | ✅ 호출 가능 — Live 데이터(`is_test=false`)만 처리 | 양 환경이 **서로 다른 데이터 풀**을 본다. Sandbox에서 만든 책/주문/크레딧은 Live에서 안 보임 | | **B** | **공통 자원** | ✅ 호출 가능 — 단일 데이터 풀 처리 | ✅ 호출 가능 — 단일 데이터 풀 처리 | **양 환경이 같은 데이터**를 본다. 계정·템플릿 등 환경 무관 공통 자원. ⚠️ Sandbox에서 운영 데이터 직접 영향 가능 | | **C** | **외부 — 테스트 모드** | ✅ 호출 가능 — 외부 시스템에 **테스트 모드**로 호출 (실 부수효과 X) | ✅ 호출 가능 — 외부 시스템에 운영 모드로 호출 | 알림톡·SMS 등. Sandbox 호출은 안전 (실제 발송 안 됨) | | **D** | **Sandbox 차단** | ❌ **501 + `ERR_SANDBOX_UNSUPPORTED`** | ✅ 정상 동작 | Sandbox에서 호출하면 차단됨. 외부 결제망 등 실 부수효과를 안전하게 막을 수 없는 경우 | > ℹ️ ✅ = "호출 자체가 통과한다"는 의미. 데이터에 어떤 영향이 가는지는 분류(A/B/C/D)에 따라 다릅니다. 단순 호출 가능 여부만 보지 말고 분류도 같이 확인하세요. ## Sandbox에서 호출 불가능한 엔드포인트 분류 D에 해당하는 엔드포인트만 모음. **Sandbox 도메인에서 호출 시 501 + `ERR_SANDBOX_UNSUPPORTED`** 반환. | 메서드 | 경로 | 사유 | | --- | --- | --- | | `POST` | `/payments/inicis/confirm` | Sandbox 호출 시 외부 결제망에 그대로 통신되기 때문에 안전 차단 필요 | 위 외 **모든 엔드포인트는 Sandbox에서 호출 가능**합니다. 다만 데이터 영향은 분류(A/B/C)에 따라 다릅니다. > ⚠️ **자동 재시도 주의:** HTTP 501은 일반적으로 "서버 미구현" 의미로 해석되지만, 본 케이스(`ERR_SANDBOX_UNSUPPORTED`)는 *의도적 차단*이라 재시도해도 동일 결과입니다. **자동 재시도 로직 화이트리스트에서 제외**하세요. 응답 분기는 HTTP 상태가 아닌 `errorCode`로 하는 것을 권장합니다. ## 컨트롤러별 매트릭스 ### 표 읽는 법 | 컬럼 | 의미 | | --- | --- | | **메서드 / 경로** | HTTP 메서드 + 경로 | | **Sandbox** | Sandbox 도메인에서 호출 결과 (✅ = 가능, ❌ = 차단) | | **Live** | Live 도메인에서 호출 결과 (✅ = 가능) | | **분류** | 위 "한눈에 보기" 표의 A/B/C/D | | **비고** | 보조 설명 | ### Accounts (`/accounts`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `POST` | `/accounts/images` | ✅ | ✅ | B | 이미지 업로드 | | `GET` | `/accounts/images` | ✅ | ✅ | B | | | `DELETE` | `/accounts/images` | ✅ | ✅ | B | | | `PATCH` | `/accounts/images` | ✅ | ✅ | B | | | `GET` | `/accounts/me` | ✅ | ✅ | B | personal도 Live OK | | `PATCH` | `/accounts/me` | ✅ | ✅ | B | | | `DELETE` | `/accounts/me` | ✅ | ✅ | B | | ### AccountConfigs (`/account-configs`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/account-configs/me` | ✅ | ✅ | B | | | `POST` | `/account-configs` | ✅ | ✅ | B | | | `PATCH` | `/account-configs/{accountUid}` | ✅ | ✅ | B | | ### ApiKeys (`/keys`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/keys` | ✅ | ✅ | A | 키마다 `env`(`test`/`live`) 필드. 양 환경 별도 풀 | | `POST` | `/keys` | ✅ | ✅ | A | 발급 시 도메인 환경에 맞춘 env로 저장 | | `PATCH` | `/keys/{keyPrefix}` | ✅ | ✅ | A | | | `DELETE` | `/keys/{keyPrefix}` | ✅ | ✅ | A | | ### Books (`/books`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/books` | ✅ | ✅ | A | | | `POST` | `/books` | ✅ | ✅ | A | 생성 시 도메인 환경에 맞춰 `is_test` 저장 | | `GET` | `/books/{bookUid}` | ✅ | ✅ | A | | | `POST` | `/books/{bookUid}/finalization` | ✅ | ✅ | A | | | `DELETE` | `/books/{bookUid}` | ✅ | ✅ | A | | | `PATCH` | `/books/{bookUid}/pdf-status` | ✅ | ✅ | A | | | `POST` | `/books/{bookUid}/pdf-result` | ✅ | ✅ | A | 레거시 | | `POST` | `/books/{bookUid}/cover` | ✅ | ✅ | A | | | `POST` | `/books/{bookUid}/contents` | ✅ | ✅ | A | | | `DELETE` | `/books/{bookUid}/contents` | ✅ | ✅ | A | | | `POST` | `/books/{bookUid}/photos` | ✅ | ✅ | A | 환경 불일치 시 403 `ERR_ENV_MISMATCH` | | `GET` | `/books/{bookUid}/photos` | ✅ | ✅ | A | | | `GET` | `/books/{bookUid}/photos/{fileName}` | ✅ | ✅ | A | | | `DELETE` | `/books/{bookUid}/photos/{fileName}` | ✅ | ✅ | A | | | `POST` | `/books/{bookUid}/pdf-cover` | ✅ | ✅ | A | | | `POST` | `/books/{bookUid}/pdf-contents` | ✅ | ✅ | A | | | `PUT` | `/books/{bookUid}/pdf-cover` | ✅ | ✅ | A | | | `PUT` | `/books/{bookUid}/pdf-contents` | ✅ | ✅ | A | | | `GET` | `/books/{bookUid}/pdf-cover` | ✅ | ✅ | A | | | `GET` | `/books/{bookUid}/pdf-contents` | ✅ | ✅ | A | | ### BookSpecs (`/book-specs`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/book-specs` | ✅ | ✅ | B | 공통 카탈로그 | | `GET` | `/book-specs/{bookSpecUid}` | ✅ | ✅ | B | | ### Templates (`/templates`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/templates` | ✅ | ✅ | B | | | `GET` | `/templates/{templateUid}` | ✅ | ✅ | B | | ### TemplateCategories (`/template-categories`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/template-categories` | ✅ | ✅ | B | | | `GET` | `/template-categories/{key}` | ✅ | ✅ | B | | ### SpecProfiles (`/spec-profiles`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/spec-profiles` | ✅ | ✅ | B | | ### Orders (`/orders`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `POST` | `/orders` | ✅ | ✅ | A | 생성 시 도메인 환경에 맞춰 `is_test` 저장 | | `POST` | `/orders/estimate` | ✅ | ✅ | A | | | `GET` | `/orders` | ✅ | ✅ | A | | | `GET` | `/orders/{orderUid}` | ✅ | ✅ | A | | | `POST` | `/orders/{orderUid}/cancel` | ✅ | ✅ | A | | | `POST` | `/orders/{orderUid}/items/{itemUid}/cancel` | ✅ | ✅ | A | | | `PATCH` | `/orders/{orderUid}/shipping` | ✅ | ✅ | A | | | `PATCH` | `/orders/{orderUid}/status` | ✅ | ✅ | A | | ### Credits (`/credits`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/credits` | ✅ | ✅ | A | 양 환경 별도 잔액 | | `GET` | `/credits/transactions` | ✅ | ✅ | A | | | `POST` | `/credits/sandbox/charge` | ✅ | (의미 없음) | — | Sandbox 전용 테스트 충전 | | `POST` | `/credits/sandbox/deduct` | ✅ | (의미 없음) | — | Sandbox 전용 테스트 차감 | ### Payments (`/payments`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `POST` | `/payments/inicis/confirm` | ❌ **501** | ✅ | **D** | `ERR_SANDBOX_UNSUPPORTED` 반환. Sandbox에서 외부 결제망 호출 방지 | ### Notifications (`/notifications`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `GET` | `/notifications/config` | ✅ | ✅ | B | | | `PATCH` | `/notifications/config` | ✅ | ✅ | B | | | `GET` | `/notifications` | ✅ | ✅ | B | | | `POST` | `/notifications/send` | ✅ | ✅ | **C** | Sandbox 호출 시 외부 발송 시스템 테스트 모드로 전달 — 실 SMS/알림톡 안 나감 | ### Webhooks (`/webhooks`) | 메서드 | 경로 | Sandbox | Live | 분류 | 비고 | | --- | --- | --- | --- | --- | --- | | `PUT` | `/webhooks/config` | ✅ | ✅ | B | | | `GET` | `/webhooks/config` | ✅ | ✅ | B | | | `DELETE` | `/webhooks/config` | ✅ | ✅ | B | | | `POST` | `/webhooks/test` | ✅ | ✅ | B | 사용자가 등록한 본인 webhookUrl로 실 HTTP 송신 (양 환경 동일) | | `GET` | `/webhooks/deliveries` | ✅ | ✅ | B | | ## 자주 헷갈리는 케이스 (FAQ) ### Q: Sandbox에서 `POST /orders` 호출하면 Live 운영 주문이 만들어지나요? 아니요. 분류 A(환경 분리)이므로 Sandbox에서 만든 주문은 `is_test=true`로 저장되며 Live에서 안 보입니다. ### Q: Sandbox에서 `POST /notifications/send` 호출하면 실제로 SMS가 발송되나요? 아니요. 분류 C(외부 — 테스트 모드)로 외부 발송 시스템에 테스트 모드로 전달되어 실 발송이 일어나지 않습니다. ### Q: Sandbox에서 `POST /payments/inicis/confirm` 호출하면 어떻게 되나요? **501 + `ERR_SANDBOX_UNSUPPORTED`** 반환되고 액션은 실행되지 않습니다. 결제 검증은 Live에서만 가능합니다. ### Q: Sandbox에서 만든 책을 Live 도메인으로 호출해서 조회 가능한가요? 아니요. 환경 격리가 적용된 모든 책 관련 API는 도메인과 책의 `is_test`가 일치해야 합니다 (`ERR_ENV_MISMATCH` 403). --- # 멱등성 (Idempotency) 네트워크 오류 · 타임아웃 · 클라이언트 재시도로 인한 중복 요청을 방지하기 위해 Book Print API가 제공하는 `Idempotency-Key` 헤더 규약을 안내합니다. ## 멱등성이란? 동일한 요청을 여러 번 보내도 서버 상태가 한 번 요청한 것과 동일하게 유지되는 성질입니다. 주문 이중 생성이나 충전금 이중 차감 같은 사고를 방지하려면 멱등성을 보장하는 메커니즘이 필요합니다. | 메서드 | 기본 멱등성 | 설명 | | --- | --- | --- | | `GET` | 멱등 | 조회만 — 안전하게 재시도 가능 | | `PUT` | 멱등 | 전체 교체 — 같은 요청 반복해도 결과 동일 | | `DELETE` | 멱등 | 이미 삭제된 리소스 재요청 시 404 또는 동일 결과 | | `POST` | **비멱등** | 새 리소스를 생성 — 재시도 시 중복 생성 위험. `Idempotency-Key` 헤더로 보완 | ## `Idempotency-Key` 헤더 중복 방지가 필요한 POST 요청에 `Idempotency-Key` HTTP 헤더를 포함해 호출합니다. 서버는 해당 키로 첫 응답을 **24시간 동안 캐시**하고, 같은 키로 재요청이 들어오면 첫 응답을 그대로 반환합니다. ### 동작 규칙 - **키 스코프**: 계정(API Key 소유자) + 엔드포인트(HTTP 메서드+경로) + Idempotency-Key 조합. 다른 계정이 같은 키를 써도 서로 영향 없음 - **캐시 유지 시간**: 첫 응답 저장 후 **24시간** - **캐시 대상**: 2xx(성공) · 4xx(클라이언트 오류) 응답이 캐시됩니다. 5xx(서버 오류)는 캐시되지 않아 재시도 시 실제 처리 - **키 길이·형식**: 임의의 문자열. 일반적으로 UUID v4 사용 권장 - **헤더 없이 호출하면**: 멱등성 처리 생략 — 일반 요청처럼 매번 새로 실행됩니다 > ⚠️ **요청 본문 일치 검증:** 같은 키로 *다른 본문*의 요청이 들어오면 서버는 **422 `ERR_IDEMPOTENCY_KEY_MISMATCH`**를 반환합니다. > 재시도는 **동일 키 + 동일 본문**이 원칙이며, 새로운 요청에는 반드시 새 키를 발급해야 합니다. **Request (헤더 포함)** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/orders' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: 8d7f9b6c-1234-5678-90ab-cdef12345678' \\ -d '{ ... }' ``` ## 적용 엔드포인트 다음 POST 엔드포인트에서 `Idempotency-Key` 헤더를 인식합니다. 그 외 엔드포인트에 헤더를 보내도 오류는 아니며, 단순히 무시됩니다. | 엔드포인트 | 중복 방지 효과 | | --- | --- | | `POST /books` | 책 중복 생성 방지 | | `POST /books/{bookUid}/cover` | 표지 추가 중복 요청 방지 | | `POST /books/{bookUid}/contents` | 내지 추가 중복 요청 방지 | | `POST /books/{bookUid}/finalization` | 최종화 중복 요청 방지 | | `POST /orders` | 충전금 이중 차감 방지 | | `POST /orders/{orderUid}/cancel` | 전체 취소 중복 요청 방지 (환불 이중 처리 방지) | | `POST /orders/{orderUid}/items/{itemUid}/cancel` | 부분 취소 중복 요청 방지 (환불 이중 처리 방지) | ## 응답 헤더 `Idempotency-Key` 헤더를 포함해 요청한 경우 응답에 아래 헤더가 포함될 수 있습니다. 캐시된 응답이 재사용됐는지 판단할 때 참고하세요. | 헤더 | 언제 포함 | 의미 | | --- | --- | --- | | `X-Idempotent-Key` | 키가 정상 처리된 모든 응답 | 요청에서 보낸 키를 에코함 | | `X-Idempotent-Replayed` | 값이 `true`일 때만 | 캐시된 응답이 재반환된 경우. 이 헤더가 없거나 `true`가 아니면 이번 호출로 새로 처리된 응답 | 정보성 헤더입니다. 비즈니스 로직의 필수 분기 조건으로 의존하기보다, 재시도/중복 감지 목적의 관찰성 용도로 활용하세요. **Response headers 예시 (첫 호출)** ```text HTTP/2 201 x-idempotent-key: 8d7f9b6c-1234-5678-90ab-cdef12345678 content-type: application/json; charset=utf-8 ``` **Response headers 예시 (재호출, 캐시 재사용)** ```text HTTP/2 201 x-idempotent-replayed: true x-idempotent-key: 8d7f9b6c-1234-5678-90ab-cdef12345678 content-type: application/json; charset=utf-8 ``` ## 동시 요청과 `409 Conflict` 같은 `Idempotency-Key`로 **동시에** 여러 요청이 도착하면 첫 요청만 처리되고, 동시에 들어온 다른 요청들은 첫 요청의 처리 완료를 기다립니다. 첫 요청이 시간 내에 끝나지 않거나 다른 이유로 락 획득에 실패하면 `409 Conflict`가 반환됩니다. 첫 요청이 완료된 후 같은 키로 오는 *후속* 요청은 409가 아니라 캐시된 응답을 그대로 받습니다 (24시간 이내). **409 ERR_CONFLICT — 락 타임아웃** ```json { "success": false, "errorCode": "ERR_CONFLICT", "message": "Conflict", "data": null, "errors": ["동일한 요청이 처리 중입니다. 잠시 후 다시 시도하세요."], "fieldErrors": [] } ``` ## 호출 패턴별 결과 `Idempotency-Key`를 사용한 호출 시나리오와 서버 응답을 매트릭스로 정리합니다. | 호출 패턴 | 결과 | 응답 | | --- | --- | --- | | 같은 키 + **같은 본문** 재시도 | 최초 응답을 그대로 재사용(중복 처리 방지) | 최초 요청과 동일한 성공/실패 응답 + `X-Idempotent-Replayed: true` | | 같은 키 + **다른 본문** | 거부 | **422** + `ERR_IDEMPOTENCY_KEY_MISMATCH` | | 같은 키 + **동시 요청**(락 타임아웃) | 선행 요청 처리 대기 권장 | **409** + `ERR_CONFLICT` | | 다른 키 + 같은 본문 | 새 요청으로 처리 | 정상 처리(중복 차감 가능 — 주의) | > ⚠️ **재시도 원칙:** 네트워크 오류 등으로 재시도해야 하는 경우, **원본 요청과 완전히 동일한 본문**을 **같은 키**로 보내세요. 본문을 수정해야 하는 상황이면 새로운 Idempotency-Key를 발급해야 합니다. ## Best Practices - **키 생성**: 요청마다 고유한 UUID v4 사용. 파트너 내부 식별자(주문 번호 등)와 조합해도 무방 - **재시도 시**: 동일 키 재사용. 새 키 발급 시 중복 처리 위험 - **새 요청**: 내용이 달라졌으면 반드시 새 키 발급. 같은 키에 다른 본문이 들어가면 `422 ERR_IDEMPOTENCY_KEY_MISMATCH` 반환 (응답 shape은 [에러 코드 & 트러블슈팅](/docs/operations/errors)의 **케이스 G** 참조) - **키 저장·로깅**: 파트너 서버에서 키 ↔ 비즈니스 트랜잭션 매핑 로그를 남겨 두면 디버깅과 감사(audit) 추적에 유용 - **409 응답**: 에러로 처리하지 말고 "선행 요청 처리 중" 신호로 해석. 잠시 후 같은 키로 재시도하면 캐시된 응답을 받을 수 있음 - **지수 백오프**: 5xx / 네트워크 오류 시 재시도 간격을 점진적으로 늘리세요. 상세는 [Rate Limiting](/docs/operations/rate-limiting) 문서 참조 ## 멱등적 주문 생성 패턴 UUID v4 키를 발급하고, 네트워크 오류·5xx 응답에는 동일 키로 재시도하며, 409는 잠시 대기 후 재시도하는 예시입니다. **Node.js** ```javascript import { randomUUID } from 'crypto'; async function createOrderSafely(orderPayload) { const idempotencyKey = randomUUID(); const maxAttempts = 4; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const response = await fetch('https://api-sandbox.sweetbook.com/v1/orders', { method: 'POST', headers: { 'Authorization': 'Bearer {YOUR_API_KEY}', 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify(orderPayload), }); if (response.ok) { // 재사용 여부 확인 (선택적) if (response.headers.get('x-idempotent-replayed') === 'true') { console.log('캐시된 응답 재사용됨'); } return await response.json(); } if (response.status === 409) { // 선행 요청이 아직 처리 중 — 잠깐 대기 후 같은 키로 재시도 await new Promise(r => setTimeout(r, 500 * Math.pow(2, attempt))); continue; } if (response.status >= 500) { // 서버 오류 — 동일 키로 재시도 (지수 백오프) await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); continue; } // 4xx (409 제외): 재시도 불필요 const err = await response.json().catch(() => ({})); throw new Error(err.message ?? \`요청 실패: \${response.status}\`); } catch (err) { if (err.name === 'TypeError') { // 네트워크 오류 — 동일 키로 재시도 await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); continue; } throw err; } } throw new Error('최대 재시도 횟수 초과'); } ``` **Python** ```python import uuid import time import requests def create_order_safely(order_payload, api_key): idempotency_key = str(uuid.uuid4()) max_attempts = 4 url = 'https://api-sandbox.sweetbook.com/v1/orders' headers = { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', 'Idempotency-Key': idempotency_key, } for attempt in range(max_attempts): try: response = requests.post(url, json=order_payload, headers=headers, timeout=30) if response.ok: if response.headers.get('x-idempotent-replayed') == 'true': print('캐시된 응답 재사용됨') return response.json() if response.status_code == 409: time.sleep(0.5 * (2 ** attempt)) continue if response.status_code >= 500: time.sleep(2 ** attempt) continue # 4xx (409 제외): 재시도 불필요 err = response.json() if response.content else {} raise RuntimeError(err.get('message', f'요청 실패: {response.status_code}')) except requests.exceptions.ConnectionError: time.sleep(2 ** attempt) continue raise RuntimeError('최대 재시도 횟수 초과') ``` --- # 페이지네이션 가이드 목록 조회 API에서 사용하는 `limit`/`offset` 기반 페이지네이션 방식과 실제 응답 구조를 안내합니다. ## 개요 Book Print API 목록 조회는 offset 기반 페이지네이션을 사용합니다. `limit`(조회 수)과 `offset`(시작 위치) 쿼리 파라미터로 범위를 지정하며, 응답 본문의 `pagination.hasNext` 필드로 다음 페이지 존재 여부를 확인합니다. > ℹ️ 일부 엔드포인트는 페이지네이션을 지원하지 않습니다. 이 경우 응답에 `pagination` 필드가 생략됩니다. 엔드포인트별 상세는 각 API 레퍼런스 페이지를 참조하세요. ## Query 파라미터 | 파라미터 | 타입 | 기본값 | 허용 범위 | 설명 | | --- | --- | --- | --- | --- | | `limit` | `number` | `20` | 1 ~ 100 | 한 번에 조회할 항목 수 | | `offset` | `number` | `0` | 0 이상 | 건너뛸 항목 수 | ### 경계 값 위반 시 400 응답 | 상황 | 응답 메시지 | | --- | --- | | `limit`이 1~100 범위 밖 (예: 0, 101) | `유효하지 않은 limit 값 (1-100 범위)` | | `offset`이 음수 | `유효하지 않은 offset 값 (음수 불가)` | 응답 본문에 `fieldErrors` 배열이 포함되며 각 항목은 `{ field, message }` 구조입니다. 에러 응답 공통 규칙은 [API 공통 사항](/docs/api/common)을 참고하세요. ## 응답 구조 모든 list 엔드포인트는 **`data` 자체가 항목 배열**이고, 페이지네이션 메타는 **최상위 `pagination` 형제 필드**로 반환됩니다. 모든 엔드포인트가 동일한 envelope 구조를 사용하므로 클라이언트는 리소스명에 의존하지 않고 일관된 코드로 처리할 수 있습니다. > ℹ️ **list envelope 통일 (2026-05):** 이전 패턴 `data.{리소스명}`(예: `data.books`, `data.transactions`, `data.photos`) 중첩은 제거되었습니다. 모든 list 응답이 `data: [...]` + 최상위 `pagination` 형태로 통일. 자세한 마이그레이션 안내는 [변경이력](/docs/changelog)을 참고하세요. | 필드 | 타입 | 설명 | | --- | --- | --- | | `data` | `array` | 현재 페이지의 항목 배열 (모든 list 엔드포인트 공통) | | `pagination.total` | `number` | 전체 항목 수 | | `pagination.limit` | `number` | 요청한 `limit` 값(응답에 에코) | | `pagination.offset` | `number` | 요청한 `offset` 값(응답에 에코) | | `pagination.hasNext` | `boolean` | 다음 페이지 존재 여부 | **GET /books — Response** ```json { "success": true, "message": "성공", "data": [ /* ... book 항목들 */ ], "pagination": { "total": 0, "limit": 10, "offset": 0, "hasNext": false } } ``` **GET /orders — Response** ```json { "success": true, "message": "성공", "data": [ /* ... order 항목들 */ ], "pagination": { "total": 42, "limit": 20, "offset": 0, "hasNext": true } } ``` **GET /templates — Response** ```json { "success": true, "message": "성공", "data": [ /* ... template 항목들 */ ], "pagination": { "total": 120, "limit": 100, "offset": 0, "hasNext": true } } ``` ## 사용 예시 ### 첫 번째 페이지 조회 `limit=20`과 `offset=0`으로 첫 번째 페이지를 요청합니다. **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/orders?limit=20&offset=0' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 두 번째 페이지 조회 `offset`을 `limit`만큼 증가시켜 다음 페이지를 요청합니다. **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/orders?limit=20&offset=20' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 전체 목록 순회 (JavaScript) `pagination.hasNext`가 `false`가 될 때까지 반복하며 항목을 수집합니다. 모든 list 엔드포인트가 동일한 envelope을 사용하므로 리소스명에 관계없이 같은 패턴이 적용됩니다. **Orders 전체 순회 예시** ```javascript async function fetchAllOrders(apiKey) { const allOrders = []; let offset = 0; const limit = 50; while (true) { const response = await fetch( \`https://api-sandbox.sweetbook.com/v1/orders?limit=\${limit}&offset=\${offset}\`, { headers: { 'Authorization': \`Bearer \${apiKey}\` } } ); const json = await response.json(); // 모든 list 응답에서 data 자체가 배열, pagination은 최상위 allOrders.push(...json.data); if (!json.pagination?.hasNext) break; offset += limit; } return allOrders; } ``` ## Best Practices - **`limit` 값은 용도에 맞게 조절하세요.** UI 표시에는 20~50, 대량 동기화에는 100을 고려할 수 있습니다. 허용 범위는 1~100입니다. - **큰 `offset` 값은 피하세요.** offset이 클수록 데이터베이스 스캔 범위가 증가해 응답이 느려질 수 있습니다. 수천 건 이상 조회가 필요하면 `createdFrom`/`createdTo` 같은 날짜 필터로 범위를 좁히는 편이 안정적입니다. - **`pagination.hasNext`를 활용하세요.** `total`로 페이지 수를 계산하는 것보다 `hasNext`로 루프 종료를 판단하는 것이 구현이 단순하고 오차 발생 여지가 적습니다. - **Rate Limit을 고려하세요.** 전체 목록을 빠르게 순회하면 일반 API 정책(300 req/분)을 초과할 수 있습니다. 상세는 [Rate Limiting](/docs/operations/rate-limiting) 문서를 참고하세요. --- # Rate Limiting 가이드 API의 안정적인 운영을 위해 엔드포인트별로 요청 빈도를 제한합니다. Rate Limit 초과 시 응답 구조와 재시도 전략을 안내합니다. ## 요청 제한 정책 Book Print API는 3개 정책으로 요청 빈도를 제한합니다. 각 정책은 엔드포인트의 특성에 따라 적용됩니다. | 정책 | 대상 | 제한 | 집계 기준 | | --- | --- | --- | --- | | **auth** | 인증 엔드포인트 (파트너 직접 호출 없음) | 10 req/분 | IP 단위 | | **general** | 일반 API (Books 조회, Orders, Credits, Templates, Webhooks 등) | 300 req/분 | API Key 단위 | | **upload** | 파일 업로드 및 콘텐츠 추가 (아래 표 참조) | 200 req/분 | API Key 단위 | ### upload 정책 적용 엔드포인트 | 엔드포인트 | 용도 | | --- | --- | | `POST /books/{bookUid}/cover` | 표지 추가 (템플릿 기반) | | `POST /books/{bookUid}/contents` | 내지 추가 (템플릿 기반) | | `POST /books/{bookUid}/pdf-cover` (POST/PUT) | 표지 PDF 업로드/교체 | | `POST /books/{bookUid}/pdf-contents` (POST/PUT) | 내지 PDF 업로드/교체 | | `POST /books/{bookUid}/photos` | 사진 업로드 | > ℹ️ **집계 기준**: general · upload 정책은 동일 API Key 소유자 단위로 집계됩니다. > 여러 서버에서 같은 Key를 사용하면 합산되므로, 높은 처리량이 필요하다면 여러 Key 발급을 고려하세요. ## Rate Limit 초과 시 응답 요청 제한을 초과하면 `429 Too Many Requests` 응답이 반환됩니다. 응답 헤더에 `Retry-After: 60`이 포함되며, 해당 시간(초) 이후 재요청할 수 있습니다. `Retry-After`는 초 단위 정수입니다. 현재 일괄 60초로 반환됩니다. **Error response** ```http HTTP/2 429 Retry-After: 60 content-type: application/json; charset=utf-8 { "success": false, "message": "Too Many Requests", "errors": ["요청 횟수 제한을 초과했습니다. 잠시 후 다시 시도하세요."] } ``` ## Best Practices ### 1. `Retry-After` 우선 존중 + 지수 백오프 fallback 429 응답을 받으면 **`Retry-After` 헤더 값만큼 대기**하는 것이 기본 원칙입니다. 헤더가 누락된 경우에만 지수 백오프(Exponential Backoff)를 fallback으로 사용하세요. 재시도 후에도 계속 429가 반환되면 요청 빈도를 구조적으로 낮춰야 합니다 (아래 2/3번 참조). **JavaScript** ```javascript async function requestWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status === 429) { // Retry-After 헤더 우선 존중 (초 단위) const retryAfterSec = parseInt(response.headers.get('Retry-After') || '0', 10); const delayMs = retryAfterSec > 0 ? retryAfterSec * 1000 : Math.pow(2, attempt) * 1000; // 헤더 없을 때만 지수 백오프 fallback console.log(\`429 received. Retrying after \${delayMs}ms...\`); await new Promise(resolve => setTimeout(resolve, delayMs)); continue; } return response; } throw new Error('Max retries exceeded'); } ``` ### 2. 응답 캐싱 변경이 잦지 않은 데이터(Templates, BookSpecs 등)는 파트너 측에 캐싱하여 불필요한 API 호출을 줄이세요. 템플릿 목록은 일반적으로 하루 단위로 캐시해도 무방합니다. ### 3. 개별 조회보다 목록 API 활용 여러 건의 데이터가 필요하면 개별 조회를 반복하지 말고 목록 API를 활용해 한 번에 받아오세요. 예를 들어 주문 10건을 확인하려면 10회 개별 조회 대신 `GET /orders?limit=10`로 1회에 처리하는 편이 효율적입니다. 페이지네이션 상세는 [페이지네이션 가이드](/docs/operations/pagination)를 참고하세요. ### 4. `Idempotency-Key`와 함께 사용 429 재시도 시 POST 엔드포인트에서 중복 처리(이중 차감 등)를 피하려면 `Idempotency-Key` 헤더를 함께 사용하세요. 같은 키로 재시도하면 첫 응답이 반환되어 안전합니다. 상세는 [멱등성 (Idempotency)](/docs/operations/idempotency) 문서를 참고하세요. --- # 에러 코드 & 트러블슈팅 Book Print API가 반환하는 HTTP 상태 코드, `errorCode` 카탈로그, 응답 shape, 트러블슈팅을 안내합니다. ## HTTP 상태 코드 Book Print API가 사용하는 HTTP 상태 코드 카탈로그입니다. 같은 HTTP 상태라도 도메인 의미에 따라 구체적인 `errorCode`가 달라지므로, 클라이언트 분기는 **HTTP 상태가 아니라 `errorCode`로** 하세요. | 코드 | 설명 | 비고 | | --- | --- | --- | | `200` | 성공 | 요청이 정상적으로 처리됨 | | `201` | 리소스 생성 성공 | 새 리소스가 생성됨 (예: 주문, 도서) | | `400` | 잘못된 요청 | 필드 검증 실패, 잘못된 파라미터 등 | | `401` | 인증 실패 | API Key가 없거나 유효하지 않음 | | `402` | 충전금 부족 | 잔액이 부족하여 주문 결제 불가 | | `403` | 권한 없음 | 해당 리소스에 대한 접근 권한 없음 / 환경 불일치 | | `404` | 리소스 없음 | 요청한 리소스를 찾을 수 없음 | | `409` | 충돌 | 리소스 상태 충돌, Idempotency 락 타임아웃 | | `422` | 처리 불가 | 동일 Idempotency-Key + 다른 요청 본문, PDF 생성 실패 | | `429` | Rate Limit 초과 | 요청 빈도 제한 초과(`Retry-After` 헤더 동봉) | | `500` | 서버 에러 | 서버 내부 오류 | | `501` | Sandbox 미지원 | Sandbox 환경에서 의도적으로 차단된 엔드포인트 호출 (Live 전용) | ## 에러 응답 형식 모든 실패 응답은 **6필드 고정 shape**으로 반환됩니다. `errorCode`·`errors`·`fieldErrors`가 **항상 존재**하므로 클라이언트에서 null 체크 없이 바로 접근할 수 있습니다. | 필드 | 타입 | 역할 | | --- | --- | --- | | `success` | boolean | 실패 응답은 항상 `false` | | `errorCode` | string | 기계 분기용 식별자. `ERR_*` 형식. HTTP 상태 기반 기본값이 자동 주입되어 항상 존재 | | `message` | string | HTTP 상태 영어 라벨 (`"Bad Request"`, `"Not Found"`, `"Forbidden"` 등). 사용자 표시용 아님 | | `data` | object | null | 기본 `null`. 일부 에러는 진단 객체 포함(예: 402 `{required, balance, currency}`) | | `errors` | string[] | 사용자 표시용 한글 상세 메시지 배열. 항상 배열(비어 있으면 `[]`) | | `fieldErrors` | object[] | 필드 단위 구조화 에러 배열. 항상 배열(해당 없으면 `[]`) | > ℹ️ 사용자에게 표시할 한글은 `errors[0]`을 사용하세요. `message`는 영어 라벨이므로 사용자 노출에 부적합합니다. 클라이언트 분기는 **`errorCode` + 필요 시 `fieldErrors[].constraint`**로 하세요. **기본 shape (실패 응답 6필드)** ```json { "success": false, "errorCode": "ERR_NOT_FOUND", "message": "Not Found", "data": null, "errors": ["해당 주문을 찾을 수 없습니다"], "fieldErrors": [] } ``` ### `fieldErrors` 항목 구조 필드 단위 검증 실패는 `fieldErrors` 배열의 각 항목이 아래 5필드를 가집니다. `field`·`message`는 항상 존재하며, 나머지 3개는 값이 있을 때만 포함됩니다. | 필드 | 타입 | 설명 | | --- | --- | --- | | `field` | string | 에러 발생 필드명 (중첩은 점 표기) | | `message` | string | 한글 설명 | | `currentValue` | any | 요청에 담겨 온 실제 값(있을 때만) | | `requiredValue` | any | 요구 값 또는 허용 범위(단일 값 또는 `{min, increment}` 객체) | | `constraint` | string | 제약 종류 enum: `required` / `min` / `max` / `increment` / `enum` / `pattern` | ## `errorCode` 카탈로그 파트너에게 반환되는 `errorCode` 21종(Generic 8 + Specific 13)의 한눈 매핑입니다. 각 코드의 응답 예시·처리 가이드는 아래 [errorCode 케이스 상세](#errorCode-cases)에서 확인하세요. | HTTP | `errorCode` | 카테고리 | 발생 조건 | 주요 발생 엔드포인트 | 재시도 | | --- | --- | --- | --- | --- | --- | | 400 | `ERR_VALIDATION_FAILED` | Generic | 일반 검증 실패 (Specific 코드 없을 때 기본값) | 모든 POST·PATCH·PUT (요청 본문/파라미터 검증) | X | | 400 | `ERR_MALFORMED_REQUEST` | Generic | 요청 형식 오류 (필드 누락, `null` 등) | 모든 엔드포인트 | X | | 400 | `ERR_INSUFFICIENT_PAGES` | Specific | 최소 페이지 미달 (`pageCount < pageMin`) | `POST /books/{bookUid}/finalization` | X | | 400 | `ERR_PAGECOUNT_INVALID` | Specific | 최대 초과 또는 증분 규칙 위반 | `POST /books/{bookUid}/finalization`, `/contents` | X | | 400 | `ERR_FINALIZE_PREREQ_UNMET` | Specific | finalize 전제조건 미충족 (PDF 미업로드, DRAFT 아님 등) | `POST /books/{bookUid}/finalization` | X | | 400 | `ERR_CREATION_TYPE_UNSUPPORTED` | Specific | 책의 `creationType`과 호출 API가 호환되지 않음 | 표지/내지/사진/PDF 추가 (`cover`·`contents`·`photos`·`pdf-cover`·`pdf-contents`) | X | | 401 | `ERR_UNAUTHORIZED` | Generic | 인증 실패 (API Key 누락·무효 등) | 모든 엔드포인트 | X | | 402 | `ERR_INSUFFICIENT_CREDIT` | Specific | 충전금 잔액 부족 (`data`에 `required`·`balance`·`currency` 동봉) | `POST /orders`, `POST /orders/estimate` | X | | 403 | `ERR_FORBIDDEN` | Generic | 권한 부족 (다른 파트너 자원 접근) | 모든 자원 접근 엔드포인트 | X | | 403 | `ERR_ENV_MISMATCH` | Specific | 호출 도메인(sandbox/live)과 대상 리소스의 환경 불일치 | 책·사진·PDF 자원 접근 | X | | 404 | `ERR_NOT_FOUND` | Generic | 리소스 미존재 | 모든 단건 조회/수정/삭제 엔드포인트 | X | | 404 | `ERR_PDF_NOT_UPLOADED` | Specific | PDF 다운로드 시 업로드형 PDF가 디스크에 없음 (그룹 A — PDF_UPLOAD/MIX 내지) | `GET /books/{bookUid}/pdf-cover`, `/pdf-contents` | X | | 409 | `ERR_CONFLICT` | Generic | 리소스 상태 충돌 (PDF 재등록), Idempotency 락 타임아웃 | `POST /books/{bookUid}/pdf-cover`, `/pdf-contents`, 모든 `[Idempotent]` 엔드포인트 | 일부 O | | 409 | `ERR_PDF_NOT_GENERATED` | Specific | PDF 다운로드 시 책 최종화 호출 전 (그룹 B — TEMPLATE/MIX 표지) | `GET /books/{bookUid}/pdf-cover`, `/pdf-contents` | O (finalize 후) | | 409 | `ERR_PDF_PENDING` | Specific | PDF 다운로드 시 백그라운드 생성 진행 중 (그룹 B) | 동일 (그룹 B) | O (간격 두고) | | 422 | `ERR_IDEMPOTENCY_KEY_MISMATCH` | Specific | 동일 `Idempotency-Key`로 다른 요청 본문 전송 | `[Idempotent]` 적용 엔드포인트 (POST /books, /cover, /contents, /finalization, /orders, /orders/{uid}/cancel 등) | X | | 422 | `ERR_PDF_GENERATION_FAILED` | Specific | PDF 다운로드 시 백그라운드 생성 실패 (그룹 B). 클라이언트 자체 해결 불가 | `GET /books/{bookUid}/pdf-cover`, `/pdf-contents` (그룹 B) | X (고객지원) | | 429 | `ERR_TOO_MANY_REQUESTS` | Generic | Rate Limit 초과 (`Retry-After` 헤더 동봉) | 모든 엔드포인트 | O (Retry-After 후) | | 500 | `ERR_INTERNAL_ERROR` | Generic | 서버 내부 오류 (미처리 예외 폴백) | 모든 엔드포인트 | O (Backoff) | | 500 | `ERR_PDF_FILE_MISSING` | Specific | PDF 다운로드 시 생성 완료(`pdfStatus=2` COMPLETED)인데 디스크에 파일 부재 (그룹 B — 서버 결함) | `GET /books/{bookUid}/pdf-cover`, `/pdf-contents` (그룹 B) | X (고객지원) | | 501 | `ERR_SANDBOX_UNSUPPORTED` | Specific | Sandbox 환경에서 의도적으로 차단된 엔드포인트 (Live 전용) | 현재 파트너 노출 엔드포인트 중 0건 (관리자 전용 결제 확인 한정) | X (Live로) | 엔드포인트별 환경(Sandbox/Live) 호출 가능 여부는 [환경 매트릭스](/docs/operations/environment-matrix)를 참조하세요. Retry 전략·Backoff·Idempotency 결합 등 운영 패턴은 [에러 핸들링 & Retry 전략](/docs/operations/error-handling)을 참조하세요. ## `errorCode` 케이스 상세 `errorCode`별로 발생 조건·응답 예시·처리 가이드를 정리합니다. 응답 받았을 때 `errorCode`로 검색하여 해당 카드를 참조하세요. ### `ERR_VALIDATION_FAILED` · HTTP 400 · Generic 일반 검증 실패. 필수 필드 누락·잘못된 값·형식 불일치 등 Specific 코드가 더 구체적이지 않은 모든 검증 실패에서 반환됩니다. `fieldErrors` 배열에서 `field`·`constraint`를 확인하여 사용자에게 구체 수정 안내를 제공하세요. **재시도 불가** — 요청 본문 수정 필요. **400 ERR_VALIDATION_FAILED** ```json { "success": false, "errorCode": "ERR_VALIDATION_FAILED", "message": "Bad Request", "data": null, "errors": ["입력값을 확인해 주세요"], "fieldErrors": [ { "field": "title", "message": "필수 항목입니다", "constraint": "required" } ] } ``` ### `ERR_MALFORMED_REQUEST` · HTTP 400 · Generic 요청 본문 형식 자체 오류. JSON 파싱 실패, 잘못된 Content-Type, 필수 헤더 누락 등 ModelBinding 단계에서 거부된 요청. **재시도 불가** — 요청 형식 수정 필요. **400 ERR_MALFORMED_REQUEST** ```json { "success": false, "errorCode": "ERR_MALFORMED_REQUEST", "message": "Bad Request", "data": null, "errors": ["요청 형식이 올바르지 않습니다"], "fieldErrors": [] } ``` ### `ERR_INSUFFICIENT_PAGES` · HTTP 400 · Specific finalize 직전 최소 페이지 미달 감지. `fieldErrors`의 `pageCount`에 `currentValue`·`requiredValue`·`constraint: "min"`이 구조화되어 있어 UI에 바로 매핑 가능합니다. **처리**: 사용자에게 "최소 N페이지 필요" 안내 후 콘텐츠 추가하여 재시도. **400 ERR_INSUFFICIENT_PAGES** ```json { "success": false, "errorCode": "ERR_INSUFFICIENT_PAGES", "message": "Bad Request", "data": null, "errors": ["최소 페이지 미달: 현재 10p, 최소 24p"], "fieldErrors": [ { "field": "pageCount", "message": "최소 24 페이지가 필요합니다", "currentValue": 10, "requiredValue": 24, "constraint": "min" } ] } ``` ### `ERR_PAGECOUNT_INVALID` · HTTP 400 · Specific 최대 페이지 초과 또는 증분 규칙 위반(예: 짝수 단위만 허용). `fieldErrors`의 `constraint`로 `max` / `increment` 구분. **처리**: `pageMin`~`pageMax` 범위와 `pageIncrement` 단위 확인 후 재시도. **400 ERR_PAGECOUNT_INVALID — increment 위반** ```json { "success": false, "errorCode": "ERR_PAGECOUNT_INVALID", "message": "Bad Request", "data": null, "errors": ["페이지 수는 2의 배수여야 합니다"], "fieldErrors": [ { "field": "pageCount", "message": "2의 배수만 허용됩니다", "currentValue": 25, "requiredValue": { "min": 24, "increment": 2 }, "constraint": "increment" } ] } ``` ### `ERR_FINALIZE_PREREQ_UNMET` · HTTP 400 · Specific finalize 전제조건 미충족 — PDF 미업로드(PDF_UPLOAD 책), DRAFT 상태 아님 등. 다중 필드가 미충족인 경우 `fieldErrors`에 모두 누적되며, 하나라도 충족되면 해당 항목은 제외됩니다. **처리**: `fieldErrors`에서 누락 항목별로 준비 단계 완료 후 재시도. **400 ERR_FINALIZE_PREREQ_UNMET — 표지/내지 PDF 미업로드** ```json { "success": false, "errorCode": "ERR_FINALIZE_PREREQ_UNMET", "message": "Bad Request", "data": null, "errors": [ "coverPdf: 표지 PDF가 업로드되지 않았습니다", "contentsPdf: 내지 PDF가 업로드되지 않았습니다" ], "fieldErrors": [ { "field": "coverPdf", "message": "표지 PDF가 업로드되지 않았습니다", "requiredValue": "uploaded", "constraint": "required" }, { "field": "contentsPdf", "message": "내지 PDF가 업로드되지 않았습니다", "requiredValue": "uploaded", "constraint": "required" } ] } ``` ### `ERR_CREATION_TYPE_UNSUPPORTED` · HTTP 400 · Specific 책의 `creationType`(`TEMPLATE`/`PDF_UPLOAD`/`MIX_COVER_TEMPLATE`)과 호출 API가 호환되지 않을 때 (예: `PDF_UPLOAD` 책에 `POST /cover` 템플릿 호출). **재시도 불가** — 책 생성 시 `creationType` 또는 호출 API 변경 필요. 자세한 호환성은 [창작 방식](/docs/concepts/creation-types)을 참조하세요. **400 ERR_CREATION_TYPE_UNSUPPORTED** ```json { "success": false, "errorCode": "ERR_CREATION_TYPE_UNSUPPORTED", "message": "Bad Request", "data": null, "errors": ["이 책의 creationType(PDF_UPLOAD)에서는 사진 업로드를 지원하지 않습니다"], "fieldErrors": [] } ``` ### `ERR_UNAUTHORIZED` · HTTP 401 · Generic `Authorization` 헤더 누락, `Bearer` 접두사 누락, 폐기·만료된 API Key. **환경 불일치**(Sandbox Key로 Live URL 호출 등)는 401이 아니라 **403 `ERR_ENV_MISMATCH`**로 분기됩니다. **재시도 불가** — `Authorization: Bearer ` 형식 확인, 포털에서 Key 상태 확인 후 새 요청. **401 ERR_UNAUTHORIZED** ```json { "success": false, "errorCode": "ERR_UNAUTHORIZED", "message": "Unauthorized", "data": null, "errors": ["인증이 필요합니다"], "fieldErrors": [] } ``` ### `ERR_INSUFFICIENT_CREDIT` · HTTP 402 · Specific 주문 생성 시 충전금 잔액 부족. 이 에러는 예외적으로 `data`에 진단 객체(`required`·`balance`·`currency`)가 들어있어 충전 UI로 바로 안내할 수 있습니다. **재시도 불가** — 파트너 포털에서 충전 후 새 요청. **402 ERR_INSUFFICIENT_CREDIT** ```json { "success": false, "errorCode": "ERR_INSUFFICIENT_CREDIT", "message": "Insufficient Credit", "data": { "required": 5500, "balance": 1820.00, "currency": "KRW" }, "errors": ["잔액이 부족합니다. 필요: 5500, 잔액: 1820.00"], "fieldErrors": [] } ``` ### `ERR_FORBIDDEN` · HTTP 403 · Generic 본인 소유가 아닌 자원에 접근하려 한 경우. **재시도 불가** — 자원 소유권 확인 후 본인 자원으로 재요청. **403 ERR_FORBIDDEN** ```json { "success": false, "errorCode": "ERR_FORBIDDEN", "message": "Forbidden", "data": null, "errors": ["해당 리소스에 대한 권한이 없습니다"], "fieldErrors": [] } ``` ### `ERR_ENV_MISMATCH` · HTTP 403 · Specific Sandbox 도메인에서 Live 리소스를 접근하거나 그 반대인 경우. 401(잘못된 키)과 명확히 구분되므로 클라이언트는 "도메인 재선택"만 안내하면 됩니다. **재시도 불가** — `api-sandbox.sweetbook.com`과 `api.sweetbook.com` 중 키에 맞는 쪽으로 새 요청. **403 ERR_ENV_MISMATCH** ```json { "success": false, "errorCode": "ERR_ENV_MISMATCH", "message": "Forbidden", "data": null, "errors": [ "환경 불일치: 호출 도메인과 API Key의 환경(sandbox/live)이 일치하지 않습니다. 키에 맞는 도메인으로 재시도하세요." ], "fieldErrors": [] } ``` ### `ERR_NOT_FOUND` · HTTP 404 · Generic 요청한 리소스(도서, 주문, 사진 등)를 찾을 수 없음. `errors[0]` 문구로 어떤 리소스가 누락됐는지 식별 가능합니다. **재시도 불가** — 생성 응답에서 받은 UID가 맞는지, 같은 환경에서 조회하는지 확인. **404 ERR_NOT_FOUND — 도서 조회** ```json { "success": false, "errorCode": "ERR_NOT_FOUND", "message": "Not Found", "data": null, "errors": ["책을 찾을 수 없습니다: bk_xxx"], "fieldErrors": [] } ``` **참고:** PDF 다운로드 시 그룹 A(`PDF_UPLOAD`/MIX 내지)에서 파일이 없는 경우는 더 구체적인 [`ERR_PDF_NOT_UPLOADED`](#ERR_PDF_NOT_UPLOADED)가 반환됩니다. ### `ERR_PDF_NOT_UPLOADED` · HTTP 404 · Specific PDF 다운로드 시 업로드형 PDF가 디스크에 없음. **그룹 A**(`PDF_UPLOAD` cover/contents + `MIX_COVER_TEMPLATE` 내지)에서만 발생. **재시도 불가** — `POST /books/{bookUid}/pdf-cover` 또는 `/pdf-contents`로 PDF 업로드 후 새 요청. **404 ERR_PDF_NOT_UPLOADED (그룹 A)** ```json { "success": false, "errorCode": "ERR_PDF_NOT_UPLOADED", "message": "Not Found", "data": null, "errors": ["PDF 파일이 업로드되지 않았습니다."], "fieldErrors": [] } ``` ### `ERR_CONFLICT` · HTTP 409 · Generic 두 가지 상황에서 반환됩니다. #### ① 리소스 이미 존재 (재시도 불가) **PDF 업로드 POST**(신규 등록 전용)를 이미 등록된 책에 다시 호출. 교체하려면 `PUT` 메서드를 사용하세요. 같은 요청을 재시도해도 같은 결과. **409 ERR_CONFLICT — PDF 표지 POST 중복** ```json { "success": false, "errorCode": "ERR_CONFLICT", "message": "Conflict", "data": null, "errors": ["표지 PDF가 이미 등록되어 있습니다. 교체하려면 PUT 메서드를 사용하세요."], "fieldErrors": [] } ``` #### ② Idempotency 락 타임아웃 (재시도 가능) 동일한 `Idempotency-Key`로 **동시 요청**이 들어와 선행 요청의 분산 락이 잡혀 있는 경우. 첫 요청이 완료되기를 기다렸다가 같은 키로 재시도하면 캐시된 응답이 반환됩니다. 상세는 [멱등성 (Idempotency)](/docs/operations/idempotency) 참조. **409 ERR_CONFLICT — Idempotency 락 타임아웃** ```json { "success": false, "errorCode": "ERR_CONFLICT", "message": "Conflict", "data": null, "errors": ["동일한 요청이 처리 중입니다. 잠시 후 다시 시도하세요."], "fieldErrors": [] } ``` **참고:** 같은 키로 *다른 본문*을 보낸 경우는 409가 아니라 **422 [`ERR_IDEMPOTENCY_KEY_MISMATCH`](#ERR_IDEMPOTENCY_KEY_MISMATCH)**로 반환됩니다. ### `ERR_PDF_NOT_GENERATED` · HTTP 409 · Specific PDF 다운로드 시 책 최종화(`POST /finalization`) 호출 전. **그룹 B**(`TEMPLATE` cover/contents + `MIX_COVER_TEMPLATE` 표지)에서만 발생. **처리**: `POST /books/{bookUid}/finalization` 호출 후 재시도. **409 ERR_PDF_NOT_GENERATED (그룹 B)** ```json { "success": false, "errorCode": "ERR_PDF_NOT_GENERATED", "message": "Conflict", "data": null, "errors": ["책 최종화 전이라 PDF가 존재하지 않습니다."], "fieldErrors": [] } ``` ### `ERR_PDF_PENDING` · HTTP 409 · Specific PDF 다운로드 시 백그라운드 생성 진행 중. **그룹 B**에서만 발생. **처리**: 일정 간격(예: 10초~수십 초) 후 재시도. 응답에는 `Retry-After` 헤더가 포함되지 않으므로 클라이언트가 자체 대기 간격을 결정해야 합니다. **무한 폴링 금지** — 횟수·총 대기 시간 제한 필수. **409 ERR_PDF_PENDING (그룹 B)** ```json { "success": false, "errorCode": "ERR_PDF_PENDING", "message": "Conflict", "data": null, "errors": ["PDF 생성이 진행 중입니다. 잠시 후 다시 시도해 주세요."], "fieldErrors": [] } ``` ### `ERR_IDEMPOTENCY_KEY_MISMATCH` · HTTP 422 · Specific 같은 `Idempotency-Key`로 **이전과 다른 요청 본문**을 보낸 경우. 재시도는 반드시 **동일 키 + 동일 본문**으로만 가능합니다. **재시도 불가** — 본문이 달라져야 한다면 새 키 발급 후 새 요청. **422 ERR_IDEMPOTENCY_KEY_MISMATCH** ```json { "success": false, "errorCode": "ERR_IDEMPOTENCY_KEY_MISMATCH", "message": "Unprocessable Entity", "data": null, "errors": [ "동일한 Idempotency-Key에 서로 다른 요청 본문이 사용되었습니다. 같은 키는 같은 본문에만 재사용 가능합니다." ], "fieldErrors": [] } ``` ### `ERR_PDF_GENERATION_FAILED` · HTTP 422 · Specific PDF 다운로드 시 백그라운드 생성 실패. **그룹 B**에서만 발생. **클라이언트 자체 해결 불가** — 사용자에게 실패를 안내하고 고객지원팀 문의 권장. 재시도해도 같은 결과. **422 ERR_PDF_GENERATION_FAILED (그룹 B — 생성 실패)** ```json { "success": false, "errorCode": "ERR_PDF_GENERATION_FAILED", "message": "Error", "data": null, "errors": ["PDF 생성에 실패했습니다. 고객지원팀에 문의해 주세요."], "fieldErrors": [] } ``` ### `ERR_TOO_MANY_REQUESTS` · HTTP 429 · Generic 정책별(auth/general/upload) 요청 한도 초과. 응답 본문과 함께 `Retry-After` 헤더(초 단위)가 동봉됩니다. **처리**: `Retry-After` 헤더 값만큼 대기 후 재시도. 정책 상세는 [Rate Limiting 가이드](/docs/operations/rate-limiting) 참고. **429 ERR_TOO_MANY_REQUESTS (헤더 포함)** ```http HTTP/1.1 429 Too Many Requests Retry-After: 60 Content-Type: application/json; charset=utf-8 { "success": false, "errorCode": "ERR_TOO_MANY_REQUESTS", "message": "Too Many Requests", "data": null, "errors": ["요청 한도를 초과했습니다. Retry-After 헤더 값만큼 대기 후 재시도하세요."], "fieldErrors": [] } ``` ### `ERR_INTERNAL_ERROR` · HTTP 500 · Generic 서버에서 미처리 예외가 발생한 경우의 폴백 응답. 내부 정보 유출 방지를 위해 일반 메시지만 반환되며, 일시적 오류일 수 있습니다. **처리**: 짧은 간격(예: 1~2초) 후 Backoff로 재시도. 지속 발생 시 고객지원팀 문의. **500 ERR_INTERNAL_ERROR** ```json { "success": false, "errorCode": "ERR_INTERNAL_ERROR", "message": "Internal Server Error", "data": null, "errors": ["서버 오류가 발생했습니다"], "fieldErrors": [] } ``` ### `ERR_PDF_FILE_MISSING` · HTTP 500 · Specific PDF 다운로드 시 생성 완료 상태(`pdfStatus=2` COMPLETED)인데 디스크에 파일이 부재. **서버 결함**으로 클라이언트 자체 해결 불가. **처리**: 사용자에게 실패 안내 + 고객지원팀 문의. 재시도해도 같은 결과. **500 ERR_PDF_FILE_MISSING (그룹 B — 서버 결함)** ```json { "success": false, "errorCode": "ERR_PDF_FILE_MISSING", "message": "Internal Server Error", "data": null, "errors": ["PDF 파일이 존재하지 않습니다. 고객지원팀에 문의해 주세요."], "fieldErrors": [] } ``` ### `ERR_SANDBOX_UNSUPPORTED` · HTTP 501 · Specific Sandbox 환경에서 의도적으로 차단된 엔드포인트(예: 외부 결제망 호출) 호출. **Live 환경 전용** 액션이며, Sandbox에서는 실 결제 등 외부 부수효과 방지를 위해 차단. 엔드포인트별 환경 호출 가능 여부는 [환경 매트릭스](/docs/operations/environment-matrix) 참조. **재시도 주의:** HTTP 501은 일반적으로 "서버 미구현"으로 해석되지만, 본 케이스는 *의도적 미지원*이라 재시도해도 동일 결과입니다. 자동 재시도 로직에서 제외하세요. **501 ERR_SANDBOX_UNSUPPORTED** ```json { "success": false, "errorCode": "ERR_SANDBOX_UNSUPPORTED", "message": "Not Implemented", "data": null, "errors": ["이 엔드포인트는 sandbox 환경에서 지원되지 않습니다"], "fieldErrors": [] } ``` ## PDF 다운로드 — 출처별 응답 분기 (그룹 A·B) `GET /books/{bookUid}/pdf-cover`·`GET /books/{bookUid}/pdf-contents`는 책의 `creationType`에 따라 응답이 두 그룹으로 분기됩니다. 클라이언트는 **`errorCode`로 분기**해야 하며, HTTP 상태만으로 판단하면 의도와 다른 동작이 됩니다. - **그룹 A (사용자 업로드)**: `PDF_UPLOAD` cover/contents + `MIX_COVER_TEMPLATE` 내지. 업로드된 파일 존재 여부만 검사. - 200 (성공) 또는 [404 `ERR_PDF_NOT_UPLOADED`](#ERR_PDF_NOT_UPLOADED) - **그룹 B (백그라운드 생성)**: `TEMPLATE` cover/contents + `MIX_COVER_TEMPLATE` 표지. 책 최종화 후 백엔드가 PDF 렌더링 → `pdfStatus`에 따라 분기. - 200 (성공) - [409 `ERR_PDF_NOT_GENERATED`](#ERR_PDF_NOT_GENERATED) (최종화 전) - [409 `ERR_PDF_PENDING`](#ERR_PDF_PENDING) (생성 진행 중) - [422 `ERR_PDF_GENERATION_FAILED`](#ERR_PDF_GENERATION_FAILED) (생성 실패 — 고객지원) - [500 `ERR_PDF_FILE_MISSING`](#ERR_PDF_FILE_MISSING) (서버 결함 — 고객지원) 자세한 분기 표는 [Books — PDF 기반](/docs/api/books-pdf) / [Books — 템플릿 기반](/docs/api/books-template) 페이지의 PDF 다운로드 섹션을 참조하세요. ## 자주 묻는 질문 (FAQ) ### Q: HTTP 상태 코드로 분기해도 되나요? 권장하지 않습니다. 같은 400도 `ERR_INSUFFICIENT_PAGES`·`ERR_PAGECOUNT_INVALID`·`ERR_FINALIZE_PREREQ_UNMET`·`ERR_VALIDATION_FAILED` 등으로 세분화되어 있어, `errorCode`로 분기해야 정확한 사용자 안내가 가능합니다. ### Q: 응답 메시지 문자열을 `includes()`로 파싱해 분기해도 되나요? 금지합니다. `errors[0]`·`message` 문구는 다국어화·표현 정비로 언제든 변경될 수 있습니다. 분기는 **`errorCode` + `fieldErrors[].constraint`** 조합으로만 하세요. ### Q: Sandbox Key로 Live URL을 호출하면? `403 ERR_ENV_MISMATCH`가 반환됩니다. 401(키 무효)과 구분되므로 클라이언트는 "도메인만 맞춰서 재시도"하면 됩니다. Sandbox Key는 `api-sandbox.sweetbook.com`, Live Key는 `api.sweetbook.com`에서만 사용 가능합니다. ### Q: 같은 `Idempotency-Key`로 다른 본문을 보내면? `422 ERR_IDEMPOTENCY_KEY_MISMATCH`가 반환됩니다. 재시도는 반드시 **원본과 동일한 본문** + 동일 키로만 가능합니다. 본문이 달라져야 한다면 새로운 키를 발급하세요. ### Q: API Key가 노출되면? 즉시 파트너 포털에서 해당 API Key를 폐기하고 새 Key를 재발급하세요. 노출된 Key로 발생한 피해는 복구가 어려울 수 있으므로 빠른 대응이 중요합니다. ## 관련 문서 - [에러 핸들링 & Retry 전략 — — 재시도 가능/불가 분류·Backoff·Idempotency 결합·종합 코드 패턴](/docs/operations/error-handling) - [API 공통 사항 — — 응답 shape · `fieldErrors` 구조 · 멱등성 개요](/docs/api/common) - [멱등성 (Idempotency) — — Idempotency-Key 상세 동작](/docs/operations/idempotency) - [Rate Limiting — — 요청 빈도 제한 정책 (429)](/docs/operations/rate-limiting) - [충전금 운영 절차 — — 402 에러 처리 및 충전 흐름](/docs/operations/credits) - [환경 매트릭스 — — Sandbox 차단 엔드포인트 (501)](/docs/operations/environment-matrix) --- # 에러 핸들링 & Retry 전략 Book Print API 호출 시 발생하는 에러를 안정적으로 처리하기 위한 운영 패턴을 안내합니다. 재시도 가능 여부 분류, Backoff 전략, Idempotency-Key 결합, Timeout·Stop Conditions 등 복원력 있는 클라이언트 작성에 필요한 가이드입니다. > ℹ️ `errorCode` 카탈로그·응답 shape·케이스 예시는 [에러 코드 & 트러블슈팅](/docs/operations/errors)을, Idempotency-Key 동작 자체는 [멱등성](/docs/operations/idempotency)을, Rate Limit 정책은 [Rate Limiting](/docs/operations/rate-limiting)을 참고하세요. 본 페이지는 이 메커니즘들을 운영 시나리오로 엮은 종합 가이드입니다. ## 재시도 가능 vs 재시도 불가 `errorCode`를 기준으로 재시도 가능 여부를 분류합니다. **재시도 가능 에러는 transient(일시적)**이며, 재시도 불가 에러는 요청·환경·코드 변경 없이 반복해도 같은 결과가 반환됩니다. | HTTP | `errorCode` | 재시도 | 권장 동작 | | --- | --- | --- | --- | | `400` | `ERR_VALIDATION_FAILED` `ERR_MALFORMED_REQUEST` `ERR_INSUFFICIENT_PAGES` `ERR_PAGECOUNT_INVALID` `ERR_FINALIZE_PREREQ_UNMET` `ERR_CREATION_TYPE_UNSUPPORTED` | X | 요청 본문 수정 후 호출. `fieldErrors` 파싱하여 사용자 안내 | | `401` | `ERR_UNAUTHORIZED` | X | API Key 확인 (만료·환경 불일치 등). 동일 호출 재시도해도 같은 결과 | | `402` | `ERR_INSUFFICIENT_CREDIT` | 조건부 | 충전 후 재시도 가능. 자동 충전은 권장 안 함 (의도하지 않은 결제 방지) | | `403` | `ERR_FORBIDDEN` / `ERR_ENV_MISMATCH` | X | 권한·환경 설정 수정. `ERR_ENV_MISMATCH`는 호출 도메인 또는 대상 리소스 환경 확인 | | `404` | `ERR_NOT_FOUND` / `ERR_PDF_NOT_UPLOADED` | X | 리소스 UID 확인. `ERR_PDF_NOT_UPLOADED`는 PDF 업로드 후 호출 | | `409` | `ERR_CONFLICT` (리소스 상태 충돌) | X | 이미 등록된 리소스에 POST 재호출 등. 같은 요청을 재시도해도 같은 결과 — 요청 흐름 수정 필요 | | `409` (Idempotency 락 타임아웃 — **응답에 `errorCode` 필드 부재**) | O | 같은 `Idempotency-Key`로 동시 호출 시 락 대기 타임아웃. 일정 간격 후 같은 키로 재시도하면 첫 응답 재사용. 자세한 분기 안내는 아래 alert 참조 | | | `ERR_PDF_NOT_GENERATED` | O | `POST /finalization` 호출 후 재시도 | | | `ERR_PDF_PENDING` | O | 일정 간격(예: 2초~10초) 후 재시도. 무한 폴링 금지 | | | `422` | `ERR_IDEMPOTENCY_KEY_MISMATCH` | X | 본문이 바뀌어야 한다면 **새 Idempotency-Key 발급** 후 재시도 | | `ERR_PDF_GENERATION_FAILED` | X | 클라이언트 자체 해결 불가 — 고객지원팀 문의 | | | `429` | `ERR_TOO_MANY_REQUESTS` | O | `Retry-After` 헤더 값(초)만큼 대기 후 재시도. Backoff 결합 권장 | | `500` | `ERR_INTERNAL_ERROR` | O | 서버 측 일시적 문제. Backoff 적용 재시도. 지속 시 고객지원팀 문의 | | `ERR_PDF_FILE_MISSING` | X | 서버 결함 — 고객지원팀 문의 | | | `501` | `ERR_SANDBOX_UNSUPPORTED` | X | 의도적 Sandbox 차단. 자동 재시도 화이트리스트 제외 필수 | > ⚠️ **HTTP 상태가 아니라 `errorCode`로 분기:** 같은 HTTP 상태라도 Specific `errorCode`에 따라 재시도 가능 여부가 다릅니다 (예: 500의 `ERR_INTERNAL_ERROR`는 재시도 O, `ERR_PDF_FILE_MISSING`은 재시도 X). 카탈로그 전체는 [에러 코드 & 트러블슈팅](/docs/operations/errors#errorCode-catalog)을 참조하세요. > ⚠️ **409 응답 shape 비대칭:** `ERR_CONFLICT`(리소스 상태 충돌)는 6필드 응답이지만, **Idempotency 락 타임아웃 응답은 `errorCode` 필드가 부재**합니다 (현재 서버 동작). 두 시나리오를 구분하려면 응답 본문에 `errorCode`가 있는지 먼저 확인하고, 없으면 락 타임아웃으로 분기해 같은 `Idempotency-Key`로 재시도하세요. 6필드 통일은 향후 서버 개선 예정. ## Exponential Backoff + Jitter transient 에러(409·429·500 등) 재시도 시 대기 시간을 **지수적으로 증가**시키고 약간의 무작위(**jitter**)를 추가합니다. jitter는 여러 클라이언트가 동시에 재시도해 서버에 부하를 주는 thundering herd를 방지합니다. - **초기 대기 시간:** 1초 권장 - **증가율:** 2배씩 (1s → 2s → 4s → 8s ...) - **최대 대기 시간:** 30초 권장 (운영 SLA에 맞게 조정) - **최대 시도 횟수:** 3~5회 권장 - **jitter:** 계산된 대기 시간의 ±20% 무작위 가산 **429 응답**의 경우 서버가 `Retry-After` 헤더(초)를 동봉합니다. 이 값을 우선 사용하고, 그 이상 대기가 필요하면 Backoff를 결합합니다. **Node.js — Exponential Backoff + Jitter** ```javascript async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function computeBackoff(attempt, retryAfterSec) { // 429 응답에 Retry-After 헤더 있으면 우선 사용 if (retryAfterSec) { return retryAfterSec * 1000; } // 1초 → 2초 → 4초 → 8초 ... 최대 30초 const base = Math.min(1000 * Math.pow(2, attempt), 30000); // jitter ±20% const jitter = base * 0.2 * (Math.random() * 2 - 1); return base + jitter; } // errorCode 기반으로 재시도 가능 여부 판정 async function isRetryable(response) { if (response.status === 429) return true; if (response.status === 500) { // 500은 ERR_INTERNAL_ERROR(재시도 O) vs ERR_PDF_FILE_MISSING(재시도 X) 분기 const body = await response.clone().json().catch(() => null); return body?.errorCode !== 'ERR_PDF_FILE_MISSING'; } if (response.status === 409) { const body = await response.clone().json().catch(() => null); // 락 타임아웃 (errorCode 필드 부재) → 재시도 O if (!body?.errorCode) return true; // ERR_PDF_NOT_GENERATED / ERR_PDF_PENDING → 재시도 O if (body.errorCode === 'ERR_PDF_NOT_GENERATED' || body.errorCode === 'ERR_PDF_PENDING') return true; // ERR_CONFLICT (리소스 상태 충돌) → 재시도 X return false; } return false; } async function withRetry(fetchFn, maxAttempts = 4) { let lastResponse; for (let attempt = 0; attempt < maxAttempts; attempt++) { lastResponse = await fetchFn(); if (!(await isRetryable(lastResponse))) { return lastResponse; } if (attempt === maxAttempts - 1) { return lastResponse; // 마지막 시도, 그대로 반환 } const retryAfter = lastResponse.headers.get('Retry-After'); const waitMs = computeBackoff(attempt, retryAfter ? parseInt(retryAfter, 10) : null); await sleep(waitMs); } return lastResponse; // 도달하지 않지만 컴파일러 경고 회피 } ``` ## Idempotency-Key + Retry 결합 네트워크 오류 또는 transient 에러로 인한 재시도 시 **같은 `Idempotency-Key`를 재사용**하면 서버가 첫 응답을 캐시해 중복 처리를 방지합니다. `POST /orders`·`POST /orders/{orderUid}/cancel`·`POST /orders/{orderUid}/items/{itemUid}/cancel` 등 부수효과 있는 호출에는 반드시 활용하세요. 적용 엔드포인트 전체 목록은 [멱등성 (Idempotency)](/docs/operations/idempotency)을 참고하세요. - **같은 키 + 같은 본문:** 첫 응답이 그대로 재사용됨 (멱등 동작) - **같은 키 + 다른 본문:** `422 ERR_IDEMPOTENCY_KEY_MISMATCH` 반환 → 본문이 바뀌어야 한다면 **새 키 발급** - **키 유효 기간:** 첫 응답 캐시 보유 60초(`LockExpiry`). 다른 클라이언트가 같은 키로 동시 호출하면 락 획득 30초 대기(`LockTimeout`) — 타임아웃 시 409 응답(위 alert 참조) 상세 동작과 적용 엔드포인트는 [멱등성 (Idempotency)](/docs/operations/idempotency)을 참조하세요. **Node.js — Idempotency-Key + Retry** ```javascript // crypto.randomUUID()는 Node.js 14.17+ / 브라우저 표준 API. 별도 라이브러리 불요. async function createOrderWithRetry(orderData) { const idempotencyKey = crypto.randomUUID(); const baseUrl = 'https://api-sandbox.sweetbook.com'; const headers = { 'Authorization': 'Bearer {YOUR_API_KEY}', 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json; charset=utf-8', }; return await withRetry(async () => { return fetch(\`\${baseUrl}/v1/orders\`, { method: 'POST', headers, body: JSON.stringify(orderData), }); }); } ``` ## Rate Limit (429) 대응 `429 ERR_TOO_MANY_REQUESTS` 응답에는 `Retry-After` 헤더가 동봉됩니다. 헤더 값(초)만큼 대기한 뒤 재시도하세요. - `Retry-After` 헤더 값을 우선 사용 - 헤더 값 이상 대기가 필요하면 Backoff 적용 - 대량 호출 패턴(예: 일괄 책 생성)에서는 **요청 간 간격을 두는 throttle**을 클라이언트 측에 구현 권장 요청 제한 정책(분당 제한값·버킷 단위 등)은 [Rate Limiting](/docs/operations/rate-limiting)을 참조하세요. ## Timeout & Stop Conditions 무한 대기·무한 재시도를 방지하려면 다음 stop conditions를 클라이언트에 적용하세요. - **한 시도당 timeout:** 단일 HTTP 호출이 일정 시간 안에 응답 안 하면 끊고 재시도. 운영 SLA에 맞게 결정 (일반적으로 30초 전후) - **전체 deadline:** 재시도 포함 전체 작업의 최대 시간. 사용자 응답 흐름에서는 짧게(예: 5분), 백그라운드 작업은 길게 설정 가능 - **최대 시도 횟수:** 3~5회 권장. 그 이상은 transient 에러가 아닐 가능성이 높으므로 클라이언트에서 알람·로그를 통해 확인 ## 자동 재시도 화이트리스트 제외 아래 에러는 클라이언트의 자동 재시도 로직에서 **의도적으로 제외**해야 합니다. 재시도해도 같은 결과가 반환되거나 의도하지 않은 부수효과가 발생합니다. | 코드 | 이유 | 대응 | | --- | --- | --- | | `501 ERR_SANDBOX_UNSUPPORTED` | 의도적 Sandbox 차단 (Live 전용 엔드포인트) | 호출 환경 확인. [환경 매트릭스](/docs/operations/environment-matrix) 참조 | | `422 ERR_IDEMPOTENCY_KEY_MISMATCH` | 같은 Idempotency-Key에 다른 본문 — 자동 재시도해도 본문 동일하지 않은 한 같은 결과 | 본문이 바뀌어야 한다면 새 키 발급. 같은 본문이라면 첫 응답 사용 | | `422 ERR_PDF_GENERATION_FAILED` | 서버 측 PDF 생성 실패 — 클라이언트 자체 해결 불가 | 고객지원팀 문의 | | `500 ERR_PDF_FILE_MISSING` | 서버 결함 (생성 완료 상태인데 파일 부재) | 고객지원팀 문의 | ## 종합 코드 패턴 — 견적 → 주문 (Retry + Backoff + Idempotency) 앞서 다룬 메커니즘들을 결합한 운영 패턴 예시입니다. `POST /orders/estimate`로 잔액 검증 후 `POST /orders`로 주문 생성하며, transient 에러는 자동 재시도합니다. - 견적은 read-only(`order:read`)이므로 멱등 안전 — Idempotency-Key 불필요 - 주문 생성은 부수효과 있음 — Idempotency-Key 필수, 재시도 시 같은 키 재사용 - 402(잔액 부족)는 자동 재시도 X — 클라이언트가 충전 안내. 충전금 운영 절차는 [충전금 운영 절차](/docs/operations/credits) 참조 **Node.js — 종합 패턴** ```javascript // withRetry / sleep / computeBackoff / isRetryable 헬퍼는 위 "Exponential Backoff + Jitter" 섹션 코드 사용. async function placeOrder(orderData) { const baseUrl = 'https://api-sandbox.sweetbook.com'; const headers = { 'Authorization': 'Bearer {YOUR_API_KEY}', 'Content-Type': 'application/json; charset=utf-8', }; // 1. 견적 (멱등 안전, Idempotency-Key 불요. transient 에러만 재시도) const estResponse = await withRetry(() => fetch(\`\${baseUrl}/v1/orders/estimate\`, { method: 'POST', headers, body: JSON.stringify(orderData), }) ); const est = await estResponse.json(); if (!est.data?.creditSufficient) { // 402 사전 차단 (자동 충전 금지) throw new Error( \`충전금 부족. 필요: \${est.data.paidCreditAmount}, 잔액: \${est.data.creditBalance}\` ); } // 2. 주문 생성 (Idempotency-Key 필수, transient 에러는 같은 키로 재시도) const idempotencyKey = crypto.randomUUID(); const orderResponse = await withRetry(() => fetch(\`\${baseUrl}/v1/orders\`, { method: 'POST', headers: { ...headers, 'Idempotency-Key': idempotencyKey }, body: JSON.stringify(orderData), }) ); // 3. 402 재확인 (견적 통과 후 동시 차감 케이스) if (orderResponse.status === 402) { const body = await orderResponse.json(); throw new Error( \`충전금 부족 (동시성). 필요: \${body.data.required}, 잔액: \${body.data.balance}\` ); } return orderResponse.json(); } ``` ## 관련 문서 - [에러 코드 & 트러블슈팅 — — errorCode 카탈로그·응답 shape·케이스 8건](/docs/operations/errors) - [멱등성 (Idempotency) — — Idempotency-Key 동작·적용 엔드포인트](/docs/operations/idempotency) - [Rate Limiting — — 요청 제한 정책·Retry-After](/docs/operations/rate-limiting) - [충전금 운영 절차 — — 402 처리 + estimate 기반 잔액 검증](/docs/operations/credits) - [환경 매트릭스 — — Sandbox 차단 엔드포인트 (501)](/docs/operations/environment-matrix) --- # 운영 전환 샌드박스 환경에서 개발과 테스트를 완료한 후, 운영(Live) 환경으로 전환하는 절차입니다. ## 환경 구분 | 항목 | 샌드박스 (Sandbox) | 운영 (Live) | | --- | --- | --- | | 목적 | 개발/테스트 | 실제 주문/제작 | | 가격 | 샌드박스 전용 가격 (`sandboxPriceBase`) | 실제 가격 (`priceBase` 또는 커스텀 가격) | | 충전금 | 테스트 충전금 | 실제 충전금 | | `personal` 계정 | 사용 가능 | **사용 불가** | | 주문 후 흐름 | 결제완료(PAID)에서 멈춤 — 제작/배송 진행 안 함 | 제작 → 출고 → 배송완료까지 전체 흐름 진행 | ## 운영 환경 접근 제어 운영 환경에서는 계정 타입에 따라 API 접근이 제어됩니다. | 환경 | 동작 | | --- | --- | | 샌드박스 환경 | 모든 요청 통과 | | 운영 환경 — `personal` 계정 | **사용 불가 (403)** | | 운영 환경 — `business` 계정 | 허용 | **personal 계정 응답 (403)** ```json { "success": false, "message": "운영 환경에서는 personal 계정으로 API를 호출할 수 없습니다", "errors": ["personal 계정은 샌드박스 환경만 사용 가능합니다"] } ``` ## 전환 체크리스트 ### 1. 계정 타입 확인 - `business` 타입의 계정을 보유하고 있는지 확인 - `personal` 계정은 운영 환경에서 사용 불가 ### 2. API Key (Live) - **business** 계정으로 전환 완료 후, **파트너 포털 > 설정 > API Key**에서 운영(Live) 환경 Key 발급 - 클라이언트 코드의 API Key를 운영용으로 교체 - 샌드박스 Key와 운영 Key는 별도로 관리됨 (서로 다른 Key를 발급받음) ### 3. 충전금 잔액 - 운영 환경 충전금이 충분한지 확인 - 충전금 환경은 별도 관리됨 (샌드박스 충전금과 운영 충전금은 분리) ### 4. 웹훅 URL 업데이트 - 운영 환경용 웹훅 수신 URL로 변경 (`PUT /webhooks/config`) - 테스트 이벤트 전송으로 연동 확인 (`POST /webhooks/test`) - 서명 검증(HMAC-SHA256) 정상 동작 확인 - 주문 이벤트 수신 경로 확인: `order.created`, `order.cancelled`, `order.item_cancelled`, `shipping.departed`, `shipping.delivered` 등 핵심 9종 모두 처리 준비 완료되어 있는지 점검 ### 5. API 엔드포인트 - 클라이언트 코드의 base URL을 운영 도메인으로 변경 ### 6. 가격 확인 - `GET /book-specs`로 운영 환경 가격 확인 - 커스텀 가격이 설정되어 있으면 해당 가격이 적용됨 - 샌드박스에서는 가격 필드가 null로 반환되었을 수 있으므로, 운영에서 실제 가격 재확인 ### 7. 테스트 - 운영 환경에서 전체 워크플로우 1회 테스트 — 실제 사용하는 생성 방식에 맞춰 재현 - TEMPLATE 방식: 판형 조회 → 책 생성(`creationType=TEMPLATE`) → 표지/내지 템플릿 적용 → finalize → 주문 생성 - PDF_UPLOAD 방식: 판형 조회 → 책 생성(`creationType=PDF_UPLOAD`, `pageCount` 포함) → 표지/내지 PDF 업로드 → finalize → 주문 생성 - MIX_COVER_TEMPLATE 방식: 판형 조회 → 책 생성(`creationType=MIX_COVER_TEMPLATE`) → 표지 템플릿 + 내지 PDF 업로드 → finalize → 주문 생성 - 웹훅 이벤트 수신 확인 (적어도 `order.created`, `shipping.departed`) - 부분 취소를 운영에서 쓸 계획이라면 `POST /orders/{orderUid}/items/{itemUid}/cancel`도 1회 시험 호출 ## 관련 문서 - [환경 (Sandbox / Live) — — 환경 차이와 도메인 구분](/docs/environments) - [환경 매트릭스 — — 엔드포인트별 Sandbox / Live 호출 가능 여부](/docs/operations/environment-matrix) --- # 변경이력 (Changelog) Book Print API의 버전별 변경 내역입니다. 이전 버전의 변경 내역을 통해 API의 변화를 추적할 수 있습니다. ## v1.2.3 2026-05-13 BookSpec 사이즈 수치 정정. 응답 필드 자체는 변경 없이 **수치값만 변경**되었습니다 (Issue #3). > ℹ️ **호환성:** 응답 shape 변경 없음. 다만 이전 수치 기준으로 캐싱·하드코딩한 클라이언트는 갱신 필요. PDF 생성 측에서도 동시에 새 수치로 전환되었으므로, 신규 PDF는 새 응답값 기준으로 만들면 됩니다. ### 변경 요약 | 분류 | 항목 | 이전 | 변경 후 | | --- | --- | --- | --- | | 하드커버 (`SQUAREBOOK_HC`, `SQUAREBOOK_LAYFLAT_HC`) | `hingeGapMm` (미소) | 9.5 | **10** | | 하드커버 | `spineWidthRules[].spineWidthMm` (책등 너비) | 11 / 17 | **10 / 16** | | 소프트커버 (`PHOTOBOOK_A4_SC`, `PHOTOBOOK_A5_SC`) | `coverThicknessMm` (표지 두께) | 0.4 | **1.0** | | 소프트커버 | `pageThicknessMm` (페이지 1장당 두께) | 0.0574 | **0.054** | ※ 도련(`bleedMm`) / 싸바리 영역(`caseWrapMarginMm`) / 내지 트림 사이즈(`innerTrimWidthMm`·`innerTrimHeightMm`) 등 다른 필드는 변경 없습니다. ### 영향 받는 API - `GET /book-specs/{bookSpecUid}` / `GET /book-specs` — 신규 사이즈 필드 값이 위 표대로 변경 - `GET /book-specs/{bookSpecUid}/calculated-size?pages=N` — 사이즈 계산 결과가 새 수치 기준으로 산출 - PDF 업로드 검증 (`POST /books/{bookUid}/pdf-cover` · `POST /books/{bookUid}/pdf-contents`) — expected 사이즈가 새 수치 기준으로 자동 계산 (`pdfToleranceMm` ±1mm 동일) ### 사이즈 계산 공식 (변경 없음) 공식 자체는 기존과 동일하며 새 수치를 대입하여 계산됩니다. 자세한 계산식과 도식은 [PDF 표지/내지 사이즈](/docs/concepts/pdf-size)를, 페이지별 검산 표는 [사이즈 계산 API의 검산 예시](/docs/concepts/pdf-size-api#검산-예시)를 참고하세요. ### 호환성 / 마이그레이션 - 응답 shape·필드 추가/제거 없음 — 응답 파싱 코드 변경 불필요 - 이전 수치 기준의 캐싱·하드코딩이 있는 경우 갱신 필요 - 이전 수치 기준으로 만든 PDF는 검증 실패할 수 있음 — 새 응답값 기준으로 PDF 재생성 권장 ## v1.2.2 2026-05-12 AI 에이전트 지원 (3가지 마크다운 산출물 + 9개 페이지 AI 가드 메모) + 신규 판형 `SQUAREBOOK_LAYFLAT_HC`(레이플랫 제본) + PDF 페이지수 검증 에러 메시지 포맷 명확화. 기존 판형·API 동작은 변경 없습니다. > ℹ️ **호환성:** 기존 HTML 페이지·SDK 변경 없음. 기존 판형(SQUAREBOOK_HC, PHOTOBOOK_A4_SC, PHOTOBOOK_A5_SC)의 PDF 업로드·응답 동작도 그대로 유지. 신규 판형(SQUAREBOOK_LAYFLAT_HC)을 사용하는 클라이언트만 LAYFLAT 펼침면 PDF 규칙(`book.pageCount / 2`) 학습 필요. ### 3가지 마크다운 산출물 | 산출물 | URL | 용도 | | --- | --- | --- | | **llms.txt** (인덱스) | `/docs/llms.txt` | 전체 문서의 페이지 목록. ~3 KB / 39개 `.md` 링크 | | **llms-full.txt** (합본) | `/docs/llms-full.txt` | 모든 페이지 본문을 합친 단일 마크다운. ~470 KB | | **.md URL** (페이지별) | `/docs/{slug}.md` | 특정 페이지만 마크다운으로. URL 끝에 `.md`만 붙이면 됨 | ### AI 가드 메모 (9개 페이지) `.md` 응답에만 HTML 주석(``)으로 노출되는 가드 메모. HTML 페이지에는 표시되지 않으며 AI가 자주 추정·실수하는 포인트(예: enum 값, 상태 비교, 메시지 파싱)를 명시적 가이드로 보강합니다. - [Orders API](/docs/api/orders) · [Webhooks API](/docs/api/webhooks) · [Webhook Events](/docs/api/webhook-events) · [Credits API](/docs/api/credits) - [인증 (API Key)](/docs/authentication) - [템플릿 기반 시나리오](/docs/guides/scenario-template) · [PDF 업로드 시나리오](/docs/guides/scenario-pdf) - [에러 코드 & 트러블슈팅](/docs/operations/errors) · [환경 매트릭스](/docs/operations/environment-matrix) ### SSR 전환 (Issue #39) 36개 docs 페이지에서 `"use client"` 지시문 제거 + `usePageTitle` 훅을 `` 컴포넌트로 교체. 본문이 서버 HTML에 직접 포함되어 검색엔진 크롤러·웹 fetch 도구가 본문을 즉시 읽을 수 있습니다. (예외: `docs/changelog/page.tsx`는 useState 사용으로 client 컴포넌트 유지) ### 안내 페이지 신규 (3건) - [AI 에이전트 지원 개요](/docs/ai-agents/overview) — 3가지 산출물과 도구별 사용 흐름 - [llms.txt · llms-full.txt 사용법](/docs/ai-agents/llms-txt) — 두 산출물의 차이, 도구별 등록 방법 - [.md URL 사용법](/docs/ai-agents/md-url) — 페이지별 마크다운 fetch + AGENT INSTRUCTIONS 가드 메모 안내 ### SDKs · Demo Apps 페이지 통합 기존 `/docs/sdk` + `/docs/demo-apps` 두 페이지를 [/docs/sdks-and-demos](/docs/sdks-and-demos) 단일 페이지로 통합. 두 옛 URL은 신 URL로 permanent redirect. ### 신규 판형 — `SQUAREBOOK_LAYFLAT_HC` (레이플랫 제본) 스퀘어북 레이플랫 하드커버 판형이 추가되었습니다. 본 판형은 플랫폼 최초의 **LAYFLAT 제본** 상품이며, 기존 PUR 제본과 PDF 업로드 규칙이 다릅니다. - 내지 트림 크기 243 × 248 mm, 페이지 범위 16 ~ 46p (2p 단위) - 책등 너비: 16~18p = 11mm, 19~46p = 17mm - **내지 PDF는 펼침면(spread)**: 업로드할 PDF 페이지수 = `book.pageCount / 2` (표지는 PUR과 동일하게 펼침면 1페이지) 관련 페이지: [BookSpecs API — 카탈로그](/docs/api/book-specs) · [PDF 표지/내지 사이즈 — 레이플랫 섹션](/docs/concepts/pdf-size) · [사이즈 계산 API — 검산 예시](/docs/concepts/pdf-size-api#검산-예시) · [PDF 업로드 시나리오 — 제본 타입별 PDF 형식](/docs/guides/scenario-pdf). ### PDF 페이지수 검증 에러 메시지 포맷 변경 내지 PDF 페이지수 불일치 메시지 라벨이 더 명확하게 변경되었습니다. 메시지 문자열 파싱이 아닌 `errorCode`(`ERR_VALIDATION_FAILED`)로 분기하는 클라이언트에는 영향 없습니다. **Before (v1.2.1 이전)** ```text 내지 PDF 페이지수 불일치: 실제 10p, 예상 8p ``` **After (v1.2.2, PUR)** ```text 내지 PDF 페이지수 불일치: 올바른 페이지수 24p, 업로드된 페이지수 30p ``` **After (v1.2.2, LAYFLAT — 책 페이지 환산값 부가)** ```text 내지 PDF 페이지수 불일치: 올바른 페이지수 8p, 업로드된 페이지수 10p (책 페이지 환산: 올바른 페이지수 16p, 업로드된 페이지수 20p) ``` ※ "예상" → "올바른 페이지수" / "실제" → "업로드된 페이지수"로 명확화. LAYFLAT 등 펼침면 PDF는 PDF 페이지 단위와 책 페이지 단위가 다르므로 책 페이지 환산값을 괄호로 부가. ### 정합성 수정 — `SQUAREBOOK_HC` 검산 표 [사이즈 계산 API 페이지의 SQUAREBOOK_HC 검산 표](/docs/concepts/pdf-size-api#검산-예시)에서 `spineWidthMm` 컬럼이 소스와 1mm 어긋나 있던 부분을 정정했습니다 (10 → 11, 16 → 17). 표지 너비 컬럼(`coverWidthMm`)은 이미 정확했으며, 실제 API 응답값에는 영향이 없는 표 정합성 수정입니다. ### 호환성 / 마이그레이션 - 기존 판형(SQUAREBOOK_HC, PHOTOBOOK_A4_SC, PHOTOBOOK_A5_SC)·API 동작 변경 없음 — 클라이언트 코드 수정 불필요 - 기존 HTML 페이지 URL·라우팅 변경 없음 (SDKs/Demo Apps 옛 URL 제외, 자동 redirect) - AI 도구 활용은 선택 — 별도 설정·플러그인 없이 URL fetch만으로 동작 - 신규 판형 `SQUAREBOOK_LAYFLAT_HC`: 사용 시 PDF 업로드 흐름 LAYFLAT 차이(펼침면 PDF, 페이지수 환산) 학습 필요 - PDF 페이지수 불일치 에러 메시지 포맷 변경: `errorCode` 기반 분기는 영향 없음. 메시지 문자열 파싱 클라이언트는 신규 라벨로 갱신 필요 ## v1.2.1 2026-05-11 list 응답 envelope을 평탄 구조로 통일. 단건 GET·단건 액션·통계 리포트는 변경 없습니다. > ⚠️ **Breaking Change — list 응답 envelope 통일** > > > 영향 받는 파트너 공개 엔드포인트의 응답 구조가 평탄화됩니다. 클라이언트 코드에서 `data.{리소스명}` 접근을 `data` 직접 접근으로 변경해야 합니다. ### envelope 변경 **Before (v1.2.0 이전)** ```json { "success": true, "data": { "books": [ /* ... */ ], "pagination": { "total": 120, "limit": 20, "offset": 0, "hasNext": true } } } ``` **After (v1.2.1)** ```json { "success": true, "data": [ /* ... */ ], "pagination": { "total": 120, "limit": 20, "offset": 0, "hasNext": true } } ``` 페이지네이션이 없는 list는 `pagination` 필드가 응답에 생략됩니다. ### 영향 받는 파트너 공개 엔드포인트 | 엔드포인트 | Before `data` 키 | After | | --- | --- | --- | | `GET /books` | `books` | `data: []` | | `GET /books/{bookUid}/photos` | `photos` + `totalCount` | `data: []` + `pagination.total` | | `GET /templates` | `templates` | `data: []` | | `GET /credits/transactions` | `transactions` | `data: []` + `pagination` 신규 추가 | | `GET /webhooks/deliveries` | `deliveries` | `data: []` | ### 변경 안 되는 엔드포인트 - 단건 GET 전부 (`GET /books/{bookUid}`, `GET /orders/{orderUid}` 등) - 단건 액션 응답 전부 (POST / PATCH / DELETE) - `GET /orders` — v1.1.0 (C19)에서 이미 평탄화 완료 - 모든 실패 응답 — v1.2.0 6필드 shape 유지 ### 마이그레이션 가이드 클라이언트 코드의 list 응답 접근을 평탄화 패턴으로 변경합니다. **Before / After (JavaScript)** ```javascript // Before const res = await fetch('/v1/books?limit=20'); const { data } = await res.json(); data.books.forEach(book => { /* ... */ }); console.log(data.pagination.total); // After const res = await fetch('/v1/books?limit=20'); const json = await res.json(); json.data.forEach(book => { /* ... */ }); console.log(json.pagination.total); ``` **자동 감지 어댑터 (배포 윈도우 대응)**: 배포 직후 일시적으로 두 패턴이 섞일 수 있다면 `Array.isArray(json.data) ? json.data : json.data.books` 형태로 양쪽을 모두 처리하는 어댑터를 일시적으로 두고 운영 안정화 후 제거하세요. 모든 list 엔드포인트가 동시에 평탄화되므로 어댑터는 짧은 기간만 필요합니다. ### 관련 페이지 갱신 - [페이지네이션](/docs/operations/pagination) — 응답 구조 섹션 전면 갱신, `data` 평탄화 패턴으로 통일 - [API 공통 사항](/docs/api/common) — list envelope 통일 안내 박스 추가, 예시 3종 평탄화 - [Books API — PDF 기반](/docs/api/books-pdf), [Books API — 템플릿 기반](/docs/api/books-template) — 책 목록·사진 목록 Response 예시 갱신 - [Templates API](/docs/api/templates) — 목록 조회 Response 갱신 - [Credits API](/docs/api/credits) — 거래 내역 Response 갱신 + `pagination` 신규 노출 ## v1.2.0 2026-05-07 정보 구조(IA) 재정비와 신규 페이지 추가. API 동작/엔드포인트 변경은 없으며 Breaking Change 없음. ### 신규 페이지 - [**창작 방식 선택**](/docs/concepts/creation-types) (`/docs/concepts/creation-types`) — PDF 업로드 / 템플릿 / 혼합 방식 비교 + 의사결정 가이드 - [**충전금 모델**](/docs/concepts/credit-model) (`/docs/concepts/credit-model`) — 환경 분리·차감/환불 규칙·402 잔액 부족 동작 원리 - [**에러 핸들링 권장사항**](/docs/operations/error-handling) (`/docs/operations/error-handling`) — 재시도 가능/불가 분류·Backoff·Idempotency 결합·종합 코드 패턴 ### 페이지 위치 변경 **멱등성** 페이지가 `/docs/concepts/idempotency` → [`/docs/operations/idempotency`](/docs/operations/idempotency)로 이동했습니다. 기존 URL을 북마크/직링크로 사용 중이라면 새 경로로 갱신하세요. ### 콘텐츠 정정 - [Credits API](/docs/api/credits) — 응답 예시·필드를 실제 API 응답과 1:1 재정합 (사실 위반 정정) - [충전금 운영 절차](/docs/operations/credits) — 운영 절차 중심으로 재정리 + estimate 기반 잔액 검증 코드 패턴 ## v1.1.0 2026-04-28 Partner API v1.2 릴리스 반영. 에러 응답 6필드 shape 통일, 주문 응답 구조 개편, 책 페이지 메타 객체 도입, Photos API 다운로드, 환경 격리, Sandbox 미지원 정식 등재, 신규 errorCode 14종, 신규 엔드포인트 3개를 포함합니다. > ⚠️ **Breaking Change 4건 포함**: 기존 클라이언트 코드에 영향이 있는 변경입니다. 항목별 마이그레이션 가이드를 반드시 확인하세요. > > - **주문 응답 구조 변경** — `GET /orders` 평탄화 + `orderStatus` 문자열 enum (이하 C19) > - **책 페이지 메타 객체 도입** — `pageCount` 필드 → `pageMeta` 객체 이동 (이하 C04) > - **빈 TEMPLATE 책 finalize 응답 정정** — `500` → `400 ERR_FINALIZE_PREREQ_UNMET` (이하 C-7 핫픽스) > - **PDF 다운로드 응답 코드 세분화** — TEMPLATE/MIX 표지 PDF GET이 일괄 `404` → `409`/`422`/`500` 분리 (이하 C02) ### C03 — 에러 응답 6필드 shape 모든 실패 응답이 `success` / `errorCode` / `message` / `data` / `errors` / `fieldErrors` 6필드 고정 shape으로 통일됩니다. `errorCode`·`errors`·`fieldErrors`는 항상 존재(빈 배열 보정)하므로 `null`·`undefined` 체크가 필요하지 않습니다. | 변경 | 내용 | | --- | --- | | `errorCode` 신규 필드 | HTTP 상태 기반 기본값 자동 주입(`ERR_VALIDATION_FAILED` 등) + 도메인 specific 코드 발급(예: `ERR_INSUFFICIENT_PAGES`) | | `fieldErrors` 5필드 구조 | `field` · `message` · `currentValue` · `requiredValue` · `constraint`. 마지막 셋은 값이 있을 때만 응답에 포함 | | `constraint` enum 6종 | `min` / `max` / `increment` / `enum` / `pattern` / `required` | | ProblemDetails 폐지 | 400 ModelBinding도 6필드 shape으로 변환되어 일관 응답 | 상세는 [API 공통 사항](/docs/api/common) · [에러 코드 & 트러블슈팅](/docs/operations/errors) 페이지를 참조하세요. **실패 응답 6필드 shape** ```json { "success": false, "errorCode": "ERR_VALIDATION_FAILED", "message": "Bad Request", "data": null, "errors": ["pageCount: 페이지 수가 부족합니다"], "fieldErrors": [ { "field": "pageCount", "message": "페이지 수가 부족합니다", "currentValue": 22, "requiredValue": 24, "constraint": "min" } ] } ``` ### C19 — 주문 응답 구조 (Breaking) > ⚠️ **Breaking Change**: 기존 `data.orders[]` 래핑이 `data[]` 배열로 평탄화되고, `orderStatus`가 숫자 코드에서 문자열 enum으로 변경됩니다. #### 1. `GET /orders` 응답 평탄화 기존 `data.orders[]` + `data.pagination` 중첩 구조가 `data[]` + 최상위 `pagination`으로 평탄화되었습니다. #### 2. `orderStatus` 문자열 enum 12종 기존 숫자 코드(`20` / `25` 등)에서 문자열 enum(`"PAID"` / `"PDF_READY"` 등)으로 변경되었습니다. `orderStatusDisplay` 한글 라벨 필드도 함께 제공됩니다. #### 3. `order.restored` 웹훅 신규 추가 취소된 주문이 복원될 때 발송되는 `order.restored` 이벤트가 추가되어 웹훅 이벤트 카탈로그가 9개로 확장되었습니다. 상세 마이그레이션은 [Orders API](/docs/api/orders) 페이지의 Breaking Change 안내 박스를 참조하세요. **Before — GET /orders (v1.0)** ```json { "success": true, "data": { "orders": [ { "orderUid": "or_xxxx", "orderStatus": 20 } ], "pagination": { "total": 1, "limit": 20, "offset": 0, "hasNext": false } } } ``` **After — GET /orders (v1.1)** ```json { "success": true, "data": [ { "orderUid": "or_xxxx", "orderStatus": "PAID", "orderStatusDisplay": "결제완료" } ], "pagination": { "total": 1, "limit": 20, "offset": 0, "hasNext": false } } ``` ### C04 — 책 페이지 메타 객체 (Breaking) > ⚠️ **Breaking Change**: 책 응답의 단일 `pageCount` 필드가 `pageMeta` 객체로 이동했습니다. `POST /books` · `POST /cover` · `POST /contents` · `POST /pdf-cover` · `POST /pdf-contents` · `POST /finalization` 응답에 공통으로 적용되는 `pageMeta` 객체가 도입되었습니다. `currentPageCount`(현재 내지 페이지 수)·`pageMin`·`pageMax`·`pageIncrement`·`isValid`(페이지 제약 충족 여부) 5필드를 포함합니다. #### 신규 엔드포인트 — `GET /books/{bookUid}` 기존 `GET /books?bookUid=...` 목록 필터 방식 외에, RESTful 단건 조회 엔드포인트가 신설되었습니다. 존재하지 않는 책은 `404 ERR_NOT_FOUND`로 응답하므로 클라이언트가 명확하게 분기할 수 있습니다. 상세는 [Books — PDF 기반](/docs/api/books-pdf) · [Books — 템플릿 기반](/docs/api/books-template) 페이지를 참조하세요. **Before — POST /books 응답 (v1.0)** ```json { "success": true, "data": { "bookUid": "bk_xxxx", "pageCount": 0 } } ``` **After — POST /books 응답 (v1.1)** ```json { "success": true, "data": { "bookUid": "bk_xxxx", "pageMeta": { "currentPageCount": 0, "pageMin": 24, "pageMax": 130, "pageIncrement": 2, "isValid": false } } } ``` ### C-7 — finalize 응답 정정 (Breaking) + 전제조건 매핑 > ⚠️ **Breaking Change (핫픽스)**: 빈 TEMPLATE 책 finalize 호출 시 응답이 `500 ERR_INTERNAL_ERROR`에서 `400 ERR_FINALIZE_PREREQ_UNMET`로 변경되었습니다. 정상 finalize 케이스 영향은 없습니다. `creationType=TEMPLATE` 책에 콘텐츠를 추가하지 않은 상태에서 `POST /books/{bookUid}/finalization`을 호출하면 `fieldErrors[0].field == "contents"` + `constraint == "required"`로 정확하게 응답합니다. PDF_UPLOAD/MIX와 동일한 패턴으로 정합화되었습니다. #### 전제조건별 에러코드 매핑 (C20) finalize 실패 케이스가 errorCode + `fieldErrors` 매핑으로 명확하게 분기됩니다 — DRAFT 상태 검증, PDF 업로드 검증, 페이지 제약(`min`/`max`/`increment`) 검증. `pageMeta.isValid`는 페이지 제약만 판정하므로 finalize 가능 여부의 최종 확인은 `POST /finalization` 호출 결과로만 이루어집니다. 상세는 [Books — PDF 기반](/docs/api/books-pdf) · [Books — 템플릿 기반](/docs/api/books-template)의 책 최종화 섹션을 참조하세요. **After — 빈 TEMPLATE finalize (v1.1)** ```json HTTP/1.1 400 Bad Request { "success": false, "errorCode": "ERR_FINALIZE_PREREQ_UNMET", "message": "Bad Request", "data": null, "errors": ["contents: 내지 콘텐츠가 최소 1개 이상 필요합니다"], "fieldErrors": [ { "field": "contents", "message": "내지 콘텐츠가 최소 1개 이상 필요합니다", "currentValue": 0, "requiredValue": "at_least_one", "constraint": "required" } ] } ``` ### C02 — PDF 다운로드 응답 분기 (Breaking) > ⚠️ **Breaking Change (그룹 B만)**: TEMPLATE 책 표지·내지와 MIX_COVER_TEMPLATE 표지의 PDF GET이 일괄 `404`에서 `pdfStatus`에 따라 `409`·`422`·`500`으로 분리됩니다. 그룹 A(PDF_UPLOAD·MIX 내지)는 종전 동작 유지 + `errorCode` 추가만. `GET /books/{bookUid}/pdf-cover` · `GET /books/{bookUid}/pdf-contents` 응답이 책의 `creationType`에 따라 두 그룹으로 분기됩니다. | 그룹 | 적용 케이스 | 분기 기준 | | --- | --- | --- | | **A — 사용자 업로드** | PDF_UPLOAD 표지·내지 + MIX 내지 | 파일 존재 여부만 검사 → `200` 또는 `404 ERR_PDF_NOT_UPLOADED` | | **B — 백그라운드 생성** | TEMPLATE 표지·내지 + MIX 표지 | 책 상태 분기 → `200`/`409`/`422`/`500` | #### 신규 errorCode 5종 | errorCode | HTTP | 발생 조건 | | --- | --- | --- | | `ERR_PDF_NOT_UPLOADED` | 404 | 그룹 A — 업로드형 PDF 파일 없음 | | `ERR_PDF_NOT_GENERATED` | 409 | 그룹 B — 책 최종화 호출 전 | | `ERR_PDF_PENDING` | 409 | 그룹 B — 백그라운드 생성 진행 중 | | `ERR_PDF_GENERATION_FAILED` | 422 | 그룹 B — 생성 실패(클라이언트 자체 해결 불가) | | `ERR_PDF_FILE_MISSING` | 500 | 그룹 B — 생성 완료 + 파일 부재(서버 결함) | 기존 `404` 한 가지만 처리하던 클라이언트는 새 코드가 미처리 예외로 빠질 수 있으므로 5종 `errorCode` 분기 추가를 권장합니다. 상세는 [Books — PDF 기반](/docs/api/books-pdf) · [Books — 템플릿 기반](/docs/api/books-template)의 PDF 다운로드 섹션을 참조하세요. ### C14 — Photos API 다운로드 + 환경 격리 #### 신규 엔드포인트 — `GET /books/{bookUid}/photos/{fileName}` 업로드된 사진의 800px 썸네일 바이트를 다운로드하는 엔드포인트가 신설되었습니다. 응답은 저장된 MIME 타입의 바이너리 스트림이며, 인증 헤더가 필요하므로 브라우저 ``에 직접 사용할 수 없습니다(백엔드 프록시 권장). #### 환경 격리 가드 — `403 ERR_ENV_MISMATCH` Photos API 4개 엔드포인트(업로드/목록/다운로드/삭제)에 환경 격리 가드가 일괄 적용됩니다. 호출 도메인(Sandbox/Live)과 책의 환경이 일치하지 않으면 `403 ERR_ENV_MISMATCH`로 응답합니다. #### creationType 차단 — `400 ERR_CREATION_TYPE_UNSUPPORTED` `PDF_UPLOAD` 타입 책에 Photos API를 호출하면 `400 ERR_CREATION_TYPE_UNSUPPORTED`로 응답합니다. Photos API는 `TEMPLATE`·`MIX_COVER_TEMPLATE` 책에서만 지원됩니다. 상세는 [Books — 템플릿 기반](/docs/api/books-template)의 사진 관련 섹션을 참조하세요. ### C01 — 템플릿 파라미터 스키마 조회 #### 신규 엔드포인트 — `GET /templates/{templateUid}/schema` 템플릿 파라미터 명세를 **JSON Schema (draft-07) 형식**으로 반환하는 엔드포인트가 신설되었습니다. 기존 상세 조회(`GET /templates/{templateUid}`)의 raw `parameters` 응답과 별도로, 표준 JSON Schema 검증기·자동화 도구·코드 생성기와 바로 연동할 수 있습니다. - 응답 필드: `$schema` / `$id` / `title` / `type` / `properties` / `required` / `x-templateKind` - 5종 binding(`text` / `file` / `gallery` / `collageGallery` / `rowGallery`) → JSON Schema 표준 형식으로 변환되며 원본 `binding`은 `x-binding` 확장 필드로 보존 - `required` 판정 정규화: 원본에 명시되지 않은 키는 자동으로 필수에 포함 기존 호출자는 변경 불필요. JSON Schema 형식이 필요한 클라이언트만 새 엔드포인트를 추가로 호출하면 됩니다. 상세는 [Templates API](/docs/api/templates)의 파라미터 스키마 조회 섹션을 참조하세요. ### C09 — 템플릿 외형 썸네일 명문화 `GET /templates` · `GET /templates/{templateUid}` 응답의 `thumbnails` 객체가 정식 미리보기 수단으로 명문화되었습니다. 정적 호스팅·인증 불요·영구 URL·확장자 가변 정책을 따르며, 등록되지 않은 항목은 응답에서 omit됩니다. - 썸네일은 **사전 등록된 외형 견본**이며, 파트너가 입력한 사진의 합성 결과가 아닙니다 — 합성 결과는 책 최종화 후 PDF 다운로드로만 확인 가능합니다. - 응답에 키가 존재하면 해당 URL은 항상 유효하며, ``에 직접 사용 가능합니다. - 코드 변경 없음(이미 운영 중인 필드의 정책 확정). 상세는 [Templates API](/docs/api/templates)의 상세 조회 섹션을 참조하세요. ### C10 — Sandbox 미지원 정식 등재 Sandbox 환경에서 의도적으로 차단된 엔드포인트(외부 결제망 호출 등)에 대해 신규 errorCode `ERR_SANDBOX_UNSUPPORTED`(HTTP 501)가 도입되었습니다. Live 전용 액션이며 자동 재시도 화이트리스트에서 제외해야 합니다(재시도해도 동일 결과). 엔드포인트별 환경(Sandbox/Live) 호출 가능 여부를 한눈에 확인할 수 있는 [환경 매트릭스](/docs/operations/environment-matrix) 페이지가 새로 추가되었습니다. 도메인 판별 규칙, 분류 체계 A/B/C/D, 인증 미들웨어 차이, FAQ 등을 포함합니다. ### 기타 변경 요약 | 항목 | 내용 | | --- | --- | | 신규 errorCode 14종 | `ERR_INSUFFICIENT_PAGES` / `ERR_PAGECOUNT_INVALID` / `ERR_FINALIZE_PREREQ_UNMET` / `ERR_CREATION_TYPE_UNSUPPORTED` / `ERR_ENV_MISMATCH` / `ERR_INSUFFICIENT_CREDIT`(402, `data`에 진단 객체) / `ERR_IDEMPOTENCY_KEY_MISMATCH`(422) / `ERR_TOO_MANY_REQUESTS`(429, `Retry-After` 60초) / `ERR_SANDBOX_UNSUPPORTED`(501) / `ERR_PDF_NOT_UPLOADED` / `ERR_PDF_NOT_GENERATED` / `ERR_PDF_PENDING` / `ERR_PDF_GENERATION_FAILED` / `ERR_PDF_FILE_MISSING` | | 신규 엔드포인트 3개 | `GET /books/{bookUid}` 단건 조회 / `GET /templates/{templateUid}/schema` / `GET /books/{bookUid}/photos/{fileName}` | | 신규 페이지 1개 | [환경 매트릭스](/docs/operations/environment-matrix) (운영 참고 그룹 최상단) | | 웹훅 이벤트 9개 | `order.restored` 신규 추가 (취소된 주문 복원 시 발송) | | Idempotency-Key | 같은 키 + 다른 본문 시 `422 ERR_IDEMPOTENCY_KEY_MISMATCH` 반환 (재시도 시 반드시 동일 키 + 동일 본문 사용) | ### v1.1 → v1.2 클라이언트 마이그레이션 가이드 v1.1 시절에는 에러 분기를 HTTP 상태 코드 또는 `errors[0]` 문자열로 하는 경우가 있었습니다. v1.2부터는 `errorCode` 기반 분기로 일관되게 통합하시길 권장합니다. #### 권장 분기 패턴 TypeScript 기준 예시입니다. `errorCode`를 기본 축으로 하되, 같은 코드 안에서 세부 제약을 나눠야 할 때는 `fieldErrors[].constraint`를 조합합니다. **권장 분기 패턴** ```ts const res = await api.finalizeBook(bookUid); if (!res.success) { switch (res.errorCode) { case "ERR_INSUFFICIENT_PAGES": { const f = res.fieldErrors.find(e => e.field === "pageCount"); showError(\`최소 \${f?.requiredValue}페이지 필요 (현재 \${f?.currentValue}p)\`); break; } case "ERR_PAGECOUNT_INVALID": { const f = res.fieldErrors.find(e => e.field === "pageCount"); if (f?.constraint === "max") { showError(\`최대 \${f.requiredValue}페이지까지 가능\`); } else if (f?.constraint === "increment") { // requiredValue는 객체 형태일 수 있음: { min, increment } showError("페이지 증분 규칙을 확인하세요"); } break; } case "ERR_FINALIZE_PREREQ_UNMET": { const missing = res.fieldErrors.map(f => f.field); if (missing.includes("coverPdf")) askUploadCover(); if (missing.includes("contentsPdf")) askUploadContents(); break; } case "ERR_INSUFFICIENT_CREDIT": { // data에 진단 객체 포함 const { required, balance } = res.data; openChargeDialog({ required, balance }); break; } case "ERR_IDEMPOTENCY_KEY_MISMATCH": showError("요청 본문이 이전과 달라졌습니다. 새 Idempotency-Key로 재시도하세요."); break; case "ERR_TOO_MANY_REQUESTS": // Retry-After 헤더의 초 단위 값 대기 scheduleRetry(retryAfterSeconds); break; case "ERR_ENV_MISMATCH": showError("호출 도메인을 리소스 환경에 맞춰 재시도 (sandbox ↔ live)"); break; case "ERR_SANDBOX_UNSUPPORTED": // Sandbox에서 의도적으로 차단된 액션 — 자동 재시도 금지, Live 환경에서만 가능 showError("이 기능은 Live(운영) 환경에서만 사용할 수 있습니다."); break; case "ERR_PDF_NOT_UPLOADED": // 그룹 A: 사용자 업로드 PDF가 없음 → 업로드 안내 askUploadPdf(); break; case "ERR_PDF_NOT_GENERATED": // 그룹 B: 책 최종화 호출 전 → finalize 후 재시도 await finalizeBook(); break; case "ERR_PDF_PENDING": // 그룹 B: 백그라운드 생성 진행 중 → 일정 간격 후 재시도(무한 폴링 금지) scheduleRetry(10_000); // 예: 10초 후 재시도 break; case "ERR_PDF_GENERATION_FAILED": // 그룹 B: pdfStatus=9 (FAILED) — 클라이언트 자체 해결 불가 showError("PDF 생성에 실패했습니다. 고객지원팀에 문의해 주세요."); break; case "ERR_PDF_FILE_MISSING": // 그룹 B: 서버 결함 — 일반 500과 별개로 PDF 파일 부재 식별 showError("PDF 파일을 찾을 수 없습니다. 고객지원팀에 문의해 주세요."); break; case "ERR_UNAUTHORIZED": redirectToKeyIssue(); break; case "ERR_NOT_FOUND": showError(res.errors[0] ?? "리소스를 찾을 수 없습니다"); break; default: // 폴백: 한글 상세 메시지 표시 showError(res.errors[0] ?? res.message); } } ``` #### 금지 사항 - **메시지 문자열 파싱으로 분기하지 말 것.** 예: `res.errors[0].includes("페이지")` 같은 패턴은 금지입니다. 문구는 다국어화·표현 정비로 언제든 변경될 수 있습니다. - **HTTP 상태만으로 세분 분기하지 말 것.** 같은 400도 `ERR_INSUFFICIENT_PAGES`·`ERR_PAGECOUNT_INVALID`·`ERR_FINALIZE_PREREQ_UNMET`·`ERR_VALIDATION_FAILED` 등으로 세분화됩니다. - **응답 shape을 가정하지 말 것.** `errorCode`·`errors`·`fieldErrors`는 항상 존재하므로 optional chaining은 불필요하며, 누락 시에는 서버 버그이므로 그대로 노출하지 말고 로깅하세요. #### 마이그레이션 체크리스트 - 기존 `res.errors[0]` 읽기 → 그대로 사용 가능 (shape 유지) - 새 `res.errorCode` 기반 분기 추가 (권장) - `res.fieldErrors`로 폼 UI 하이라이트 구성 (권장) - HTTP 상태 코드로만 분기하던 로직은 `errorCode`로 대체 고려 - Idempotency 재시도는 동일 키 + 동일 본문으로만 — 본문이 달라지면 새 키 발급 - **자동 재시도 화이트리스트에서 `ERR_SANDBOX_UNSUPPORTED`(501) 제외** — 의도적 차단이라 재시도해도 동일 결과 #### 하위 호환성 요약 - 성공 응답 shape은 변경되지 않았습니다. - 기존 필드(`success` / `message` / `data` / `errors`)는 그대로 유지됩니다. - 신규 추가(`errorCode`, `fieldErrors`, `fieldErrors[].currentValue`/`requiredValue`/`constraint`)는 필드 *추가*만 된 것으로 기존 클라이언트가 깨지지 않습니다. - `errorCode`를 아직 쓰지 않는 클라이언트는 계속 `errors[0]`을 사용자에게 표시하면 동작에 문제없습니다. ## v1.0.3 2026-04-08 통합 가이드 전면 재작성, 문서 간 불일치 해소, API 소스코드 최신 변경사항 반영 ### 통합 가이드 재작성 | 변경 | 내용 | | --- | --- | | 사이드바 구조 | 7개 → **11개** 메뉴 (표지 추가, 내지 추가, 최종화 분리) | | 신규 페이지 | 표지 추가, 내지 추가, 최종화 가이드 3개 신설 | | 기존 페이지 | API 소스코드 검증된 워크플로우 md 기반으로 8개 페이지 전면 재작성 | | 엔드포인트 뱃지 | 모든 엔드포인트에 GET/POST/PUT/PATCH/DELETE 뱃지 추가 | | 다음 단계 안내 | 각 페이지 하단에 워크플로우 연결 링크 | ### 문서 정확성 개선 | 항목 | 내용 | | --- | --- | | 내부 정보 제거 | 내부 상태 코드, 클래스명, 소스 파일 참조, 관리자 전용 파라미터 전체 제거 | | 내부 필드 노출 차단 | BookSpec 9개, Book 5개, Order 18개 내부 필드 — 일반 사용자 응답에서 제거된 API 변경 반영 | | JWT / print_shop 제거 | 파트너 문서에서 불필요한 JWT 인증, print_shop 계정 타입 참조 제거 | | API Key 스코프 제거 | 인증 페이지 및 가이드 전체에서 스코프/권한 표기 제거 | | 표현 톤 조정 | personal 계정 "차단" → "사용 불가"로 변경 | ### 문서 간 불일치 해소 | 항목 | 변경 전 | 변경 후 | | --- | --- | --- | | 주문 상태 코드 | 페이지마다 9~11개 불일치 | 전체 11개 코드(20~90) 통일 | | 웹훅 이벤트명 | `order.paid`, `order.shipped` 등 | `order.created`, `shipping.departed` 등 8개 통일 | | 웹훅 페이로드 | camelCase / 구조 불일치 | snake_case + envelope 구조 통일 | | X-Webhook-Timestamp | ISO 8601 | Unix timestamp (초) | | 배송비 | 3,500원 (일부 페이지) | 3,000원 통일 | | 포장비 | 500원 (일부 페이지) | 0원 통일 | | 충전금 잔액 API | `GET /credits/balance` | `GET /credits` | | 402 에러 응답 | `data: null` | `{required, balance, currency}` | | 금액 표기 | 정수 (`3000`) | decimal (`3000.00`) | | 파일 형식 | TIFF 포함 / HEIF 누락 | TIFF 제거, HEIC/HEIF 포함 | | 자동 변환 | "모두 JPEG" | GIF/WebP→PNG, BMP/HEIC/HEIF→JPG | | templateKind | 2개 (일부 페이지) | 4개 (cover, content, divider, publish) | | 용어 | 크레딧 | 충전금 | ### 콘텐츠 보강 - **Sandbox/Live 환경 차이** — 판형 조회(가격 차이), 웹훅(환경별 설정 분리) 안내 추가 - **책 조회 API** — 책 생성 가이드에 목록/단건 조회 추가 - **Sandbox 가격 안내** — 주문 생성 가이드에 테스트 가격 안내 추가 - **페이지 수 규칙** — 계산식에 한글 설명 추가 ## v1.0.2 2026-04-03 API 문서 정확성 개선 및 Sandbox/Live 환경 전환 UI가 추가되었습니다. ### 정정 사항 | 항목 | 변경 전 | 변경 후 | | --- | --- | --- | | 배송비 | 3,500원 | **3,000원** | | 포장비 | 500원 × 수량 | **0원** (현재 비활성) | | 웹훅 이벤트 | 5종 (`order.paid` 등) | **8종** (`order.created`, `production.confirmed`, `shipping.departed` 등) | | 웹훅 서명 | HMAC 값만 | `sha256=` 접두사 추가 | | API Key 스코프명 | `books:read` (복수형) | `book:read` (단수형) | | API Key 생성 필드 | `name` | `note` | | 책 삭제 제약 | DRAFT 상태만 | 본인 소유 책 상태 무관 삭제 가능 | | 주문 취소 | `cancelReason` 선택 | 필수 (최대 500자) | | 충전금 거래 필드 | `transactionUid`, `type`, `description` | `transactionId`, `reasonCode`+`reasonDisplay`, `memo` | | Sandbox 충전금 필드 | `description` | `memo` | ### 추가 사항 | 항목 | 내용 | | --- | --- | | 충전금 reason code | 전체 11종 (1~11) 표기. 7 ORDER_CANCEL_REFUND, 8 ADMIN_CANCEL_REFUND 추가 | | 충전금 잔액 응답 | `accountUid`, `createdAt`, `updatedAt` 필드 추가 | | Sandbox 충전금 규칙 | API Key 환경과 무관하게 항상 test 충전금에 적용 | | 템플릿 파라미터 구조 | `definitions` 래퍼, `binding` 키, `$$var$$` 플레이스홀더 구문 설명 | ### UI 개선 - **Sandbox/Live 환경 탭** — 모든 API 요청 코드 예시에 Sandbox/Live URL 전환 탭 추가 ## v1.0.1 2026-04-02 API 기능 개선 릴리즈입니다. 책 삭제, 크레딧 거래 내역, Sandbox 크레딧 관리 엔드포인트가 신규 추가되었으며, API Key 스코프 시스템, 멱등성 지원, 템플릿 검색 등 여러 기능이 개선되었습니다. > ⚠️ **Breaking Change 포함:** `책 생성 요청 본문 (POST /books)`에서 `creationType` 필드가 제거되었습니다. 자세한 내용은 아래를 참고하세요. ### Breaking Change #### 1. 책 생성 요청 본문 (POST /books)에서 `creationType` 제거 책 생성 요청 본문에서 `creationType` 필드가 제거되었습니다. 해당 필드를 전송하더라도 무시되며, `bookSpecUid`만으로 책을 생성할 수 있습니다. **Before — POST /books** ```json { "bookSpecUid": "spec_abc123", "creationType": "TEMPLATE" } ``` **After — POST /books** ```json { "bookSpecUid": "spec_abc123" } ``` ### 신규 API #### 2. 책 삭제 — DELETE `/books/{bookUid}` `DRAFT` 상태인 본인 소유의 책을 삭제할 수 있습니다. 삭제는 소프트 딜리트(soft delete)로 처리됩니다. 이미 주문이 연결된 책이나 타인 소유의 책은 삭제할 수 없습니다. **Request** ```bash curl -X DELETE 'https://api-sandbox.sweetbook.com/v1/books/book_abc123' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Response (200 OK)** ```json { "success": true, "message": "책이 삭제되었습니다.", "data": null } ``` #### 3. 크레딧 거래 내역 조회 — GET `/credits/transactions` 본인 계정의 크레딧 충전·차감 거래 내역을 조회합니다. 페이지네이션(`limit`/`offset`)을 지원합니다. **Request** ```bash curl 'https://api-sandbox.sweetbook.com/v1/credits/transactions?limit=20&offset=0' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Response (200 OK)** ```json { "success": true, "message": "Success", "data": { "total": 3, "transactions": [ { "transactionUid": "txn_xyz789", "type": "CHARGE", "amount": 50000, "balanceAfter": 150000, "createdAt": "2026-04-01T09:00:00Z" } ] } } ``` #### 4. Sandbox 크레딧 충전/차감 — POST `/credits/sandbox/charge`, POST `/credits/sandbox/deduct` Sandbox 환경에서 테스트용 크레딧을 충전하거나 차감할 수 있습니다. Live 환경에서는 동작하지 않습니다. > ℹ️ Sandbox API Key로만 호출 가능합니다. Live API Key로 호출 시 `403 Forbidden`이 반환됩니다. **충전 — POST /credits/sandbox/charge** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/credits/sandbox/charge' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "amount": 10000 }' ``` **차감 — POST /credits/sandbox/deduct** ```bash curl -X POST 'https://api-sandbox.sweetbook.com/v1/credits/sandbox/deduct' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "amount": 5000 }' ``` ### 기능 개선 #### 5. API Key 스코프 시스템 API Key 생성 시 `scopes` 파라미터로 접근 가능한 엔드포인트 범위를 제한할 수 있습니다. 스코프가 설정된 키로 허용되지 않은 엔드포인트에 접근하면 `403 Forbidden`이 반환됩니다. | 스코프 | 허용 동작 | | --- | --- | | `books:read` | 책 목록/상세 조회 | | `books:write` | 책 생성, 수정, 삭제 | | `orders:read` | 주문 목록/상세 조회 | | `orders:write` | 주문 생성, 취소 | **Before — POST /keys (스코프 없음)** ```json { "env": "live" } ``` **After — POST /keys (스코프 지정)** ```json { "env": "live", "scopes": ["books:read", "orders:read"] } ``` **Response — 스코프 부족 시 (403 Forbidden)** ```json { "success": false, "message": "이 작업을 수행할 권한이 없습니다.", "data": null } ``` #### 6. 템플릿 목록 정렬/검색 파라미터 추가 GET `/templates`에 정렬 및 검색 파라미터가 추가되었습니다. | 파라미터 | 설명 | | --- | --- | | `sort` | `name_asc`, `name_desc`, `created_asc`, `updated_desc`, `updated_asc` | | `templateName` | 이름 검색 (공백 구분 다중 키워드, AND 조건) | | `specProfileUid` | 특정 판형 프로필 UID로 필터 | | `theme` | 테마로 필터 (예: `minimal`) | **Before — GET /templates** ```bash curl 'https://api.sweetbook.com/v1/templates?limit=20&offset=0' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **After — 정렬 및 검색 적용** ```bash curl 'https://api.sweetbook.com/v1/templates?sort=updated_desc&templateName=일기장&theme=minimal' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` #### 7. BookSpecs `accountUid` 자동 적용 GET `/book-specs` 조회 시 `accountUid`를 전달하지 않으면 API Key 소유자의 UID가 자동으로 적용됩니다. 커스텀 가격이 설정된 경우 자동으로 반영됩니다. 타인의 `accountUid`를 전달하면 `403 Forbidden`이 반환됩니다. **Before — accountUid 직접 전달 필요** ```bash curl 'https://api.sweetbook.com/v1/book-specs?accountUid=acc_myuid123' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **After — accountUid 생략 가능 (자동 적용)** ```bash curl 'https://api.sweetbook.com/v1/book-specs' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` #### 8. 멱등성(Idempotency-Key) 지원 확대 네트워크 재시도 시 중복 처리를 방지하기 위한 `Idempotency-Key` 헤더 지원이 확대되었습니다. 동일한 키로 재요청하면 최초 응답이 그대로 반환됩니다. 지원 엔드포인트: - POST `/books` - POST `/orders` - POST `/credits/sandbox/charge` **Before — Idempotency-Key 미지원** ```bash curl -X POST 'https://api.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "bookSpecUid": "spec_abc123" }' ``` **After — Idempotency-Key 헤더 추가** ```bash curl -X POST 'https://api.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \\ -d '{ "bookSpecUid": "spec_abc123" }' ``` #### 9. 콘텐츠 추가 응답에 `pageCount` 추가 POST `/books/{bookUid}/content` 응답에 현재까지 추가된 총 페이지 수를 나타내는 `pageCount` 필드가 포함됩니다. **Before — 콘텐츠 추가 응답** ```json { "success": true, "message": "Success", "data": { "bookUid": "book_xyz123", "status": "DRAFT" } } ``` **After — pageCount 포함** ```json { "success": true, "message": "Success", "data": { "bookUid": "book_xyz123", "status": "DRAFT", "pageCount": 4 } } ``` #### 10. 템플릿 응답에 `theme` 필드 추가 GET `/templates` 목록 및 GET `/templates/{templateUid}` 상세 응답 객체에 `theme` 필드가 추가됩니다. **Before — 템플릿 응답 객체** ```json { "templateUid": "tmpl_abc123", "name": "미니멀 일기장", "bookSpecUid": "spec_xyz" } ``` **After — theme 필드 포함** ```json { "templateUid": "tmpl_abc123", "name": "미니멀 일기장", "bookSpecUid": "spec_xyz", "theme": "minimal" } ``` ## v1.0 2026-03-24 Book Print API 최초 공개 릴리즈입니다. 책 생성부터 주문·배송까지의 전체 워크플로우를 지원하는 7개 리소스 그룹이 포함됩니다. | 리소스 | 포함 엔드포인트 | | --- | --- | | Authentication | API Key 발급 — Bearer token, IP 화이트리스트 | | Books | 생성, 표지/콘텐츠 추가, 사진 업로드, 최종화, 목록/상세/사진 조회 | | Orders | 견적 조회, 주문 생성, 취소, 배송지 변경, 목록/상세 조회 | | Templates | 목록/상세 조회 | | BookSpecs | 목록/상세 조회 | | Credits | 잔액 조회 | | Webhooks | 설정 등록/조회/삭제, 테스트 전송, 전송 이력 조회 | ### 1. Authentication (인증) 모든 API 요청은 `Authorization: Bearer {API_KEY}` 헤더를 통해 인증합니다. #### Key 형식 API Key는 `SB{10자리prefix}.{32자리secret}` 형식입니다. prefix(`SBXXXXXXXXXX`)는 키를 식별하며, secret은 생성 시 1회만 반환됩니다. 서버에는 SHA-256 해시로만 저장되므로 분실 시 재발급이 필요합니다. #### 주요 특징 - Sandbox / Live 환경별 별도 키 발급 — 환경 간 키 교차 사용 불가 - IP 화이트리스트 최대 10개 등록 가능 (미설정 시 모든 IP 허용) - 잘못된 키: `401 Unauthorized` / 화이트리스트 차단: `403 Forbidden` **인증 헤더 예시** ```bash curl 'https://api.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer SBXXXXXXXXXX.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ``` **인증 실패 응답 (401 Unauthorized)** ```json { "success": false, "message": "유효하지 않은 API Key입니다.", "data": null } ``` ### 2. Books API 책의 생성·편집·최종화까지 전체 생명주기를 관리합니다. 책은 `DRAFT` 상태에서 시작하며, POST `/books/{bookUid}/finalization` 호출 후 주문 가능한 상태가 됩니다. #### 엔드포인트 목록 | 메서드 | 경로 | 설명 | | --- | --- | --- | | `POST` | `/books` | 새 책 생성 (`bookSpecUid` 필수) | | `GET` | `/books` | 책 목록 조회 — `status`, `limit`, `offset`, `isTest` 필터 | | `GET` | `/books/{bookUid}` | 책 상세 조회 | | `POST` | `/books/{bookUid}/cover` | 표지 추가 (템플릿 + 이미지 바인딩) | | `POST` | `/books/{bookUid}/contents` | 콘텐츠(내지) 추가 — Gallery 레이아웃 지원 | | `POST` | `/books/{bookUid}/photos` | 사진 업로드 (최대 200장, 50MB/파일) | | `GET` | `/books/{bookUid}/photos` | 사진 목록 조회 | | `DELETE` | `/books/{bookUid}/photos/{photoId}` | 사진 삭제 | | `POST` | `/books/{bookUid}/finalization` | 최종화 — 편집 완료, 주문 가능 상태로 전환 | #### 이미지 업로드 방식 사진 업로드(POST `/photos`)는 세 가지 방식을 혼합하여 한 번에 전송할 수 있습니다. - **파일 업로드:** `multipart/form-data`로 직접 전송 (`$upload` 키 사용) - **URL:** 공개 접근 가능한 이미지 URL 지정 - **서버 파일명:** 이전에 업로드된 파일의 서버 측 파일명 참조 HEIC / WebP / BMP 포맷은 JPEG으로 자동 변환됩니다. EXIF 방향 정보도 자동으로 보정됩니다. MD5 해시 기반 중복 검사가 적용되어 동일 파일은 재업로드되지 않습니다. #### Gallery 레이아웃 - **Collage:** 한 페이지에 최대 9장 배치 - **Row:** 행 단위 배치, 장 수 제한 없음 #### 외부 시스템 연동 `externalRef` 파라미터를 통해 파트너 시스템의 고유 ID를 책에 연결할 수 있습니다. 목록 조회 시 해당 값으로 필터링하거나 추적이 가능합니다. **POST /books — 책 생성** ```bash curl -X POST 'https://api.sweetbook.com/v1/books' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "bookSpecUid": "spec_abc123", "externalRef": "my-order-9999" }' ``` **Response (201 Created)** ```json { "success": true, "message": "Success", "data": { "bookUid": "book_xyz789", "status": "DRAFT", "bookSpecUid": "spec_abc123", "externalRef": "my-order-9999", "createdAt": "2026-03-24T10:00:00Z" } } ``` **POST /books/{bookUid}/photos — 사진 업로드 (혼합)** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/book_xyz789/photos' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -F '$upload=@/path/to/photo1.jpg' \\ -F 'url=https://example.com/photo2.jpg' ``` **POST /books/{bookUid}/contents — Gallery 레이아웃** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/book_xyz789/contents' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "layoutType": "COLLAGE", "photos": [ { "photoId": "photo_001" }, { "photoId": "photo_002" }, { "photoId": "photo_003" } ] }' ``` ### 3. Orders API 견적 조회 후 주문을 생성하면 크레딧이 즉시 차감됩니다. 취소 시 차감된 크레딧은 자동으로 환불됩니다. #### 엔드포인트 목록 | 메서드 | 경로 | 설명 | | --- | --- | --- | | `POST` | `/orders/estimate` | 견적 조회 — 주문 전 금액 미리 확인 | | `POST` | `/orders` | 주문 생성 — 크레딧 즉시 차감 | | `GET` | `/orders` | 주문 목록 조회 | | `GET` | `/orders/{orderUid}` | 주문 상세 조회 | | `POST` | `/orders/{orderUid}/cancel` | 주문 취소 — `PAID` / `PDF_READY` 상태만 가능, 크레딧 자동 환불 | | `PATCH` | `/orders/{orderUid}/shipping` | 배송지 변경 — `PAID` / `PDF_READY` / `CONFIRMED` 상태 가능 | #### 금액 계산 공식 | 항목 | 계산식 | | --- | --- | | 제품 금액 | 단가 × 수량 | | 배송비 | 3,500원 / 주문 (수량 무관 고정) | | 포장비 | 500원 × 수량 | | 합계 | 제품 금액 + 배송비 + 포장비 | | 결제 크레딧 (VAT 포함) | `floor(합계 × 1.1 / 10) × 10` | #### 주문 상태 코드 (11종) | 코드 | 상태명 | 설명 | | --- | --- | --- | | `20` | `PAID` | 결제 완료 — 크레딧 차감됨 | | `25` | `PDF_READY` | PDF 생성 완료 — 제작 대기 중 | | `30` | `CONFIRMED` | 제작 확정 — 취소 불가 시점 | | `40` | `IN_PRODUCTION` | 제작 진행 중 | | `45` | `COMPLETED` | 항목 제작 완료 (부분 완료) | | `50` | `PRODUCTION_COMPLETE` | 전체 제작 완료 | | `60` | `SHIPPED` | 발송 완료 — 운송장 번호 제공 | | `70` | `DELIVERED` | 배송 완료 | | `80` | `CANCELLED` | 취소 처리됨 | | `81` | `CANCELLED_REFUND` | 취소 후 크레딧 환불 완료 | | `90` | `ERROR` | 오류 발생 — 고객센터 문의 필요 | **POST /orders/estimate — 견적 조회** ```bash curl -X POST 'https://api.sweetbook.com/v1/orders/estimate' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "bookUid": "book_xyz789", "quantity": 2 }' ``` **견적 응답 (200 OK)** ```json { "success": true, "message": "Success", "data": { "unitPrice": 15000, "quantity": 2, "productAmount": 30000, "shippingFee": 3500, "packagingFee": 1000, "totalAmount": 34500, "paidCreditAmount": 37950 } } ``` **POST /orders — 주문 생성** ```bash curl -X POST 'https://api.sweetbook.com/v1/orders' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "bookUid": "book_xyz789", "quantity": 2, "shipping": { "recipient": "김영수", "phone": "010-0000-0000", "address": "서울시 강남구 테헤란로 123", "zipCode": "06234" } }' ``` **POST /orders/{orderUid}/cancel — 주문 취소** ```bash curl -X POST 'https://api.sweetbook.com/v1/orders/ord_abc123/cancel' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` ### 4. Templates API 책 표지 및 내지에 적용할 수 있는 디자인 템플릿을 조회합니다. 템플릿에는 `$key$` 형식의 파라미터가 포함될 수 있으며, 표지/콘텐츠 추가 시 실제 값으로 치환하여 바인딩합니다. #### 엔드포인트 목록 | 메서드 | 경로 | 설명 | | --- | --- | --- | | `GET` | `/templates` | 템플릿 목록 조회 | | `GET` | `/templates/{templateUid}` | 템플릿 상세 조회 | #### 목록 조회 필터 파라미터 | 파라미터 | 설명 | | --- | --- | | `scope` | 템플릿 접근 범위 (`public` / `private`) | | `templateKind` | 종류: `cover` (표지) / `content` (내지) | | `category` | 카테고리 — 일기장, 알림장, 사진앨범 등 | | `bookSpecUid` | 특정 판형에 호환되는 템플릿만 필터 | | `limit` / `offset` | 페이지네이션 | #### 파라미터 바인딩 템플릿 내 `$title$`, `$date$` 등의 플레이스홀더는 표지/콘텐츠 추가 요청 시 `parameters` 객체에 키-값을 전달하여 치환됩니다. **GET /templates — 목록 조회** ```bash curl 'https://api.sweetbook.com/v1/templates?templateKind=cover&category=사진앨범&bookSpecUid=spec_abc123&limit=10' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Response (200 OK)** ```json { "success": true, "message": "Success", "data": { "total": 24, "templates": [ { "templateUid": "tmpl_001", "name": "미니멀 사진앨범 표지", "templateKind": "cover", "category": "사진앨범", "bookSpecUid": "spec_abc123", "parameters": ["$title$", "$date$"] } ] } } ``` **POST /books/{bookUid}/cover — 표지 바인딩** ```bash curl -X POST 'https://api.sweetbook.com/v1/books/book_xyz789/cover' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "templateUid": "tmpl_001", "parameters": { "$title$": "우리 가족 앨범", "$date$": "2026.03" }, "photoId": "photo_cover_001" }' ``` ### 5. BookSpecs API 책 생성에 사용할 수 있는 판형(크기·제본 방식) 목록과 가격 정보를 조회합니다. `bookSpecUid`는 책 생성 시 필수 파라미터입니다. #### 엔드포인트 목록 | 메서드 | 경로 | 설명 | | --- | --- | --- | | `GET` | `/book-specs` | 판형 목록 조회 | | `GET` | `/book-specs/{bookSpecUid}` | 판형 상세 조회 | #### 제공 판형 종류 | 판형명 | 크기 | 제본 | | --- | --- | --- | | A4 소프트커버 | 210 × 297 mm | 소프트커버 무선제본 | | A5 소프트커버 | 148 × 210 mm | 소프트커버 무선제본 | | 스퀘어북 하드커버 | 210 × 210 mm | 하드커버 양장 | | 레이플랫 하드커버 | 210 × 297 mm | 하드커버 레이플랫 | | 슬림앨범 하드커버 | 148 × 210 mm | 하드커버 양장 | #### 가격 계산 방식 기본 가격은 `pageMin`까지의 페이지를 포함합니다. 초과 페이지는 `pageIncrement` 단위로 추가되며, 초과분당 `pricePerExtraPage`가 부과됩니다. 전체 페이지 수는 `pageMin` 이상 `pageMax` 이하여야 합니다. - 단가 = 기본가 + (초과 페이지 수 ÷ `pageIncrement`) × 페이지당 추가금 - 페이지는 반드시 `pageIncrement` 배수 단위로만 추가 가능 **GET /book-specs — 판형 목록** ```bash curl 'https://api.sweetbook.com/v1/book-specs' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Response (200 OK)** ```json { "success": true, "message": "Success", "data": { "bookSpecs": [ { "bookSpecUid": "spec_abc123", "name": "A4 소프트커버", "basePrice": 12000, "pricePerExtraPage": 200, "pageMin": 24, "pageMax": 200, "pageIncrement": 4 } ] } } ``` ### 6. Credits API 주문 생성 시 사용되는 크레딧 잔액을 조회합니다. Sandbox와 Live 환경은 잔액이 완전히 분리되어 독립적으로 관리됩니다. #### 엔드포인트 | 메서드 | 경로 | 설명 | | --- | --- | --- | | `GET` | `/credits` | 현재 크레딧 잔액 조회 | #### 크레딧 운영 규칙 - 주문 생성(POST `/orders`) 시 `paidCreditAmount`만큼 즉시 차감 - 주문 취소 시 차감된 크레딧 전액 자동 환불 - 잔액 부족 시 `402 Payment Required` 반환 - Sandbox 환경 잔액은 실제 결제와 무관한 테스트 전용 크레딧 **GET /credits — 잔액 조회** ```bash curl 'https://api.sweetbook.com/v1/credits' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' ``` **Response (200 OK)** ```json { "success": true, "message": "Success", "data": { "balance": 150000, "env": "live", "currency": "KRW" } } ``` **잔액 부족 응답 (402 Payment Required)** ```json { "success": false, "message": "크레딧이 부족합니다.", "data": { "required": 37950, "balance": 10000 } } ``` ### 7. Webhooks API 주문 상태 변경 등의 이벤트가 발생할 때 지정한 URL로 HTTP POST 요청을 전송합니다. 수신 서버는 HMAC-SHA256 서명을 검증하여 요청의 신뢰성을 확인해야 합니다. #### 엔드포인트 목록 | 메서드 | 경로 | 설명 | | --- | --- | --- | | `PUT` | `/webhooks/config` | 웹훅 URL 및 secret 등록/수정 | | `GET` | `/webhooks/config` | 현재 웹훅 설정 조회 | | `DELETE` | `/webhooks/config` | 웹훅 설정 삭제 | | `POST` | `/webhooks/test` | 테스트 이벤트 즉시 전송 | | `GET` | `/webhooks/deliveries` | 전송 이력 조회 (성공/실패 포함) | #### 지원 이벤트 | 이벤트 | 발생 시점 | | --- | --- | | `order.paid` | 주문 생성 및 크레딧 차감 완료 | | `order.confirmed` | 제작 확정 — 이후 취소 불가 | | `order.status_changed` | 주문 상태 변경 시 (모든 상태 전환) | | `order.shipped` | 발송 완료 — 운송장 번호 포함 | | `order.cancelled` | 주문 취소 및 환불 처리 완료 | #### 전송 헤더 | 헤더 | 설명 | | --- | --- | | `X-Webhook-Event` | 이벤트 종류 (예: `order.paid`) | | `X-Webhook-Signature` | HMAC-SHA256 서명 (`sha256=...`) | | `X-Webhook-Timestamp` | 전송 시각 (Unix timestamp) | | `X-Webhook-Delivery-Id` | 전송 이력 고유 ID | #### 재시도 정책 수신 서버가 `2xx` 응답을 반환하지 않으면 최대 3회 재시도합니다. 재시도 간격: 1분 후 → 5분 후 → 30분 후. 3회 모두 실패하면 해당 전송은 실패 처리되며 GET `/webhooks/deliveries`에서 확인할 수 있습니다. **PUT /webhooks/config — 설정 등록** ```bash curl -X PUT 'https://api.sweetbook.com/v1/webhooks/config' \\ -H 'Authorization: Bearer {YOUR_API_KEY}' \\ -H 'Content-Type: application/json' \\ -d '{ "url": "https://your-server.com/webhooks/sweetbook", "secret": "your-webhook-secret-key" }' ``` **웹훅 페이로드 예시 (order.shipped)** ```json { "event": "order.shipped", "timestamp": "2026-03-25T14:30:00Z", "data": { "orderUid": "ord_abc123", "status": "SHIPPED", "trackingNumber": "1234567890", "carrier": "CJ대한통운" } } ``` **서명 검증 예시 (Node.js)** ```bash const crypto = require('crypto'); function verifySignature(payload, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } ``` ### 8. 플랫폼 공통 특징 v1.0 전체에 적용되는 공통 동작 및 제약 사항입니다. | 항목 | 내용 | | --- | --- | | 인증 방식 | API Key — Bearer token (`Authorization` 헤더) | | 환경 분리 | Sandbox / Live 환경 독립 운영 — 키·크레딧·데이터 모두 분리 | | 응답 형식 | JSON `{ success, message, data }` 구조 통일 | | 페이지네이션 | `limit` / `offset` 기반 (전체 목록 조회 API 공통) | | Rate Limiting | 일반 API 300 req/min, 업로드 API 200 req/min | | 날짜 형식 | ISO 8601 UTC (`2026-03-24T00:00:00Z`) | | 이미지 자동 변환 | HEIC / WebP / BMP → JPEG 자동 변환, EXIF 방향 자동 보정 | | 중복 이미지 검사 | MD5 해시 기반 — 동일 파일 재업로드 방지 | | Webhook 보안 | HMAC-SHA256 서명 (`X-Webhook-Signature`) | | Idempotency-Key | 미지원 (v1.0.1에서 추가됨) |