보따리
Docs

API Guide

# Botari API 가이드

Botari 앱/게임 연동용 REST API와 연동 규약 정리입니다.

문서에서 정리한 기준은 내부 구현 기준입니다. 운영 규칙은 정책 문서와 함께
확인하세요.

## 1. 공통

### REST 루트
- 기본: `/wp-json/`
- 운영 서비스 API 호스트: `https://api.botari.net/wp-json/`
- 보따리 라우트: `/wp-json/botari/v1/...`

### 인증
- 로그인 사용자: `X-WP-Nonce` (WordPress 비로그인 제한 경로 포함)
- 일부 공개 API만 비로그인 사용 가능

### 실행/격리 도메인
- 상품/마이페이지: `https://{user}.botari.net`
- 앱/게임 실행: `https://play.botari.net/bottariapp/{product_id}`
- 내부 정적 파일 프록시: `https://play.botari.net/bottariapp-proxy/{product_id}/{session_token}/...`
- 서비스 API: `https://api.botari.net/wp-json/botari/v1/...`

보따리앱 ZIP은 서버 파일시스템에 그대로 공개하지 않고 실행 시 토큰을 발급한 뒤
`bottariapp-proxy`를 통해 정적 파일만 제공합니다. 프록시는 토큰, 제품 ID, 파일 경로를
검증하고 PHP/쉘/실행 파일 계열 확장자를 차단합니다.

앱 내부에서는 직접 WordPress 쿠키에 의존하지 말고 `BotariSDK`를 사용하세요. SDK는 부모
페이지와 `postMessage` 브릿지로 실행 세션 토큰을 받은 뒤, 가능한 경우 Relay API로
플레이/점수 같은 서버 이벤트를 중계합니다.

## 2. 앱/게임 API

### 2.0 권장 연동 방식

보따리앱/게임은 ZIP 안에 `assets/js/botari-sdk.js`를 포함하거나 플랫폼이 제공하는 SDK를
로드한 뒤 아래 메서드를 사용합니다.

```js
await BotariSDK.recordPlay();
await BotariSDK.submitScore(1200, { level: 3 });
const rankings = await BotariSDK.getRankings(10);
await BotariSDK.setStorage('menu', { categories: [] });
const menu = await BotariSDK.getStorage('menu');
```

SDK가 실행 세션 토큰을 받으면 내부적으로 `/wp-json/botari/v1/relay`를 사용합니다. 세션
토큰이 없는 일반 페이지에서는 기존 REST 엔드포인트로 폴백할 수 있습니다.

### 2.0.1 외부 라이브러리 정책

Three.js 같은 범용 프론트엔드 라이브러리는 보따리 공식 API로 제공하지 않습니다. 보따리
플랫폼은 `BotariSDK`, Relay API, 저장소 API, 권한/결제/랭킹처럼 플랫폼과 직접 연결되는
기능만 제공합니다.

앱 제작자는 필요한 외부 라이브러리를 ZIP 내부에 포함하거나, 허용된 신뢰 CDN에서 직접
로드할 수 있습니다.

권장 방식:
- 보안/안정성이 중요하면 라이브러리 파일을 ZIP 안에 포함하고 상대 경로로 로드합니다.
- CDN을 사용할 경우 `cdn.jsdelivr.net`, `cdnjs.cloudflare.com`, `unpkg.com` 같은 허용된
  CDN만 사용합니다.
- 라이브러리 버전은 고정합니다. 예: `three@0.160.0`

Three.js 예시:

```html
<!-- CDN 방식 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>

<!-- ZIP 포함 방식 -->
<script src="./libs/three.min.js"></script>
```

### 2.0.2 SDK 우선 원칙

보따리앱 안에서는 런타임 객체나 REST 경로를 자동 탐색하지 말고 공식 `BotariSDK` 메서드를 먼저 사용합니다.

공식 SDK 메서드가 있는 기능:
- JSON 스토리지: `listStorage()`, `getStorage()`, `setStorage()`, `deleteStorage()`
- 휴대폰 소유자 인증: `sendSmsCode()`, `verifySmsCode()`
- QR/바코드 생성: `generateQR()`, `generateBarcode()`
- 게임/앱 이벤트: `recordPlay()`, `submitScore()`, `getRankings()`

직접 REST 호출은 관리 화면, 서버 사이드 코드, SDK가 없는 레거시 앱에서만 사용합니다. 특히 앱 실행 iframe 안에서는 부모 페이지 bridge와 실행 세션 토큰 처리가 필요하므로 SDK 사용을 기본값으로 둡니다.


### 2.0.3 브라우저 권한 요청 안내

카메라, 마이크, 위치, 클립보드, 푸시 알림은 앱이 실제로 해당 기능을 쓰기 직전에 사용자 클릭 흐름에서 요청합니다. 앱 시작 직후 한꺼번에 권한창을 띄우지 마세요.

권장 UI 흐름:
- 기능 버튼을 누르기 전 짧게 안내합니다. 예: `QR 스캔을 위해 카메라 권한이 필요합니다.`
- 사용자가 `허용하기`를 누른 뒤 브라우저 권한 API를 호출합니다.
- 거부되면 브라우저 사이트 설정에서 다시 허용해야 한다고 안내합니다.
- 권한이 없어도 가능한 대체 흐름을 제공합니다. 예: 카메라 스캔 대신 이미지 업로드.

기본 예시:

```js
async function requestCamera() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ video: true });
    stream.getTracks().forEach((track) => track.stop());
    return true;
  } catch (error) {
    alert('카메라 권한이 필요합니다. 브라우저 사이트 설정에서 허용해 주세요.');
    return false;
  }
}

async function requestLocation() {
  return new Promise((resolve) => {
    navigator.geolocation.getCurrentPosition(
      () => resolve(true),
      () => {
        alert('위치 권한이 필요합니다. 브라우저 사이트 설정에서 허용해 주세요.');
        resolve(false);
      }
    );
  });
}
```

보따리 실행 iframe은 카메라/마이크/위치/클립보드 권한을 사용할 수 있도록 허용 속성을 제공합니다. 브라우저별 지원 차이가 있으므로 Safari/iOS에서는 대체 흐름을 준비하세요.

### 2.1 게임 점수 제출

**Endpoint**
- `POST /wp-json/botari/v1/game/{product_id}/score`

**요청**
- `score` (number) 점수
- `metadata` (object, optional) 부가 데이터

**예시**
```js
fetch(`${botariData.root}botari/v1/game/123/score`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': botariData.nonce
  },
  body: JSON.stringify({
    score: 1200,
    metadata: { level: 3, cleared: true }
  })
});
```

### 2.2 게임 플레이 기록

**Endpoint**
- `POST /wp-json/botari/v1/games/{product_id}/play`

**비고**
- 게임 플레이 횟수/진입 추적용
- 공식 정책: 게임은 로그인 사용자만 실행 가능
- 게임 비구매자: 하루 1회 무료 실행
- 게임 구매자: 무제한 실행
- 일반 앱: 무료/구매/작성자 권한 기준으로 웹에서 바로 실행

### 2.3 게임 랭킹 조회

**Endpoint**
- `GET /wp-json/botari/v1/game/{product_id}/rankings?limit=10`
- `GET /wp-json/botari/v1/game/{product_id}/my-best-score`

## 3. 공통 릴레이 API

### 3.1 중계 엔드포인트

**Endpoint**
- `POST /wp-json/botari/v1/relay`

**목적**
- 앱 내부 이벤트 전달/인증 토큰 기반 중계
- 앱/공급자/구독자 간 실시간 이벤트 전달
- `ack` 처리 및 상태 전달에 활용

**공식 액션**
- `game:play`: 게임/앱 실행 기록
- `game:score`: 게임 점수 제출
- `storage:list`: 앱 스토리지 key 목록 조회
- `storage:get`: 앱 스토리지 JSON 조회
- `storage:set`: 앱 스토리지 JSON 저장
- `storage:delete`: 앱 스토리지 JSON 삭제

## 4. 보따리앱 JSON 스토리지 API

보따리앱이 메뉴판, 설정, 진행상태처럼 사용자별 앱 데이터를 저장할 때 사용하는 공식
저장소입니다. 서버 DB를 앱에 직접 열지 않고, 검증된 JSON API만 제공합니다.

### 4.1 저장 경계

데이터는 아래 조합으로 격리됩니다.

```text
user_id + product_id + storage_key
```

예를 들어 메뉴판 앱에서 `storage_key`를 `menu`로 쓰면, A 사장님의 메뉴판 데이터와 B
사장님의 메뉴판 데이터는 같은 앱이라도 서로 접근할 수 없습니다.

### 4.2 제한

- 저장 형식: JSON only
- key 길이: 1~100자
- key 허용 문자: 영문, 숫자, `.`, `_`, `:`, `-`
- 단일 value 크기: 최대 64KB
- 사용자-앱별 총 용량: 최대 5MB
- 권한: 제품 작성자, 관리자, 구매/내보따리 추가 사용자
- 금지: 크레딧, 구매권한, 결제상태, 승인상태 같은 민감 상태를 앱 스토리지 값으로 결정하면 안 됩니다.

### 4.3 SDK 사용

앱 실행 중에는 직접 REST 호출보다 `BotariSDK` 사용을 권장합니다. SDK는 실행 세션 토큰이
있으면 Relay API의 `storage:*` 액션을 사용하고, 일반 로그인 페이지에서는 REST로
폴백합니다.

```js
const menuData = {
  storeName: '조슈아 카페',
  categories: [
    {
      name: '커피',
      items: [
        { name: '아메리카노', price: 4500, soldOut: false }
      ]
    }
  ]
};

await BotariSDK.setStorage('menu', menuData);

const saved = await BotariSDK.getStorage('menu');
console.log(saved.value);

const items = await BotariSDK.listStorage();
console.log(items.used_bytes, items.quota_bytes);

await BotariSDK.deleteStorage('menu');
```

### 4.3.1 SDK 시그니처와 응답 스키마

앱 제작자는 아래 SDK 함수만 사용하면 됩니다. 앱 안에서 `window.botariAPI`, `botariData`, REST root, nonce를 직접 탐색하지 마세요.

```js
const list = await BotariSDK.listStorage();
const item = await BotariSDK.getStorage('menu');
const saved = await BotariSDK.setStorage('menu', { categories: [] });
const deleted = await BotariSDK.deleteStorage('menu');
```

**listStorage 응답**
```json
{
  "success": true,
  "items": [
    {
      "key": "menu",
      "version": 1,
      "bytes": 123,
      "updated_at": "2026-05-16 16:28:00"
    }
  ],
  "used_bytes": 123,
  "quota_bytes": 5242880
}
```

**getStorage 응답**
```json
{
  "success": true,
  "product_id": 123,
  "key": "menu",
  "value": { "categories": [] },
  "version": 1,
  "hash": "sha256...",
  "updated_at": "2026-05-16 16:28:00"
}
```

**setStorage 응답**
```json
{
  "success": true,
  "product_id": 123,
  "key": "menu",
  "version": 1,
  "hash": "sha256...",
  "bytes": 123,
  "updated_at": "2026-05-16 16:28:00"
}
```

**deleteStorage 응답**
```json
{
  "success": true,
  "key": "menu",
  "deleted": true
}
```

### 4.4 REST 엔드포인트

REST는 로그인 사용자와 `X-WP-Nonce`가 있는 환경에서 사용할 수 있습니다. 보따리앱 iframe 안에서는 REST를 직접 탐색하지 말고 `BotariSDK`를 사용하세요.

**목록 조회**
- `GET /wp-json/botari/v1/app-storage/{product_id}`

**값 조회**
- `GET /wp-json/botari/v1/app-storage/{product_id}/{key}`

**값 저장**
- `POST /wp-json/botari/v1/app-storage/{product_id}/{key}`
- `PUT /wp-json/botari/v1/app-storage/{product_id}/{key}`

**값 삭제**
- `DELETE /wp-json/botari/v1/app-storage/{product_id}/{key}`

**저장 요청 예시**
```js
await fetch('https://api.botari.net/wp-json/botari/v1/app-storage/123/menu', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': botariData.nonce
  },
  body: JSON.stringify({
    value: {
      storeName: '조슈아 카페',
      categories: []
    }
  })
});
```

**저장 응답 예시**
```json
{
  "success": true,
  "product_id": 123,
  "key": "menu",
  "version": 1,
  "hash": "sha256...",
  "bytes": 62,
  "updated_at": "2026-05-16 16:28:00"
}
```

**조회 응답 예시**
```json
{
  "success": true,
  "product_id": 123,
  "key": "menu",
  "value": {
    "storeName": "조슈아 카페",
    "categories": []
  },
  "version": 1,
  "hash": "sha256...",
  "updated_at": "2026-05-16 16:28:00"
}
```

## 5. QR 생성 API

### 5.1 QR SVG/PNG 생성

**Endpoint**
- `GET /wp-json/botari/v1/qr`

**필수 파라미터**
- `data` (string): QR 내용

**선택 파라미터**
- `size` (64~1024, default: 256)
- `margin` (0~10, default: 2)
- `color` (HEX without #, default: 000000)
- `bg` (HEX without #, default: ffffff)
- `format` (`svg` or `png`, default: `svg`)

**응답 규칙**
- `format=svg` (기본): PHP 내부 생성기로 `svg` 문자열 반환
- `format=png`: 기존 서버 QR 런타임이 있는 경우 `png`에 `data:image/png;base64,...` 문자열 반환
- SVG 내부 생성은 현재 약 230바이트 이하 데이터에 최적화되어 있습니다. 긴 텍스트는 QR보다 짧은 공유 URL을 넣는 방식을 권장합니다.

**SDK 예시**
```js
try {
  const qr = await BotariSDK.generateQR({
    data: 'https://botari.net/app/123',
    size: 256,
    format: 'svg'
  });
  document.querySelector('#qr').innerHTML = qr.svg;
} catch (err) {
  if (err.code === 'login_required') {
    showNotice('SVG QR 코드는 보따리 계정으로 로그인하면 만들 수 있어요.');
  }
}
```

**REST 예시**
```js
const params = new URLSearchParams({
  data: 'https://botari.net/app/123',
  size: '256',
  margin: '2',
  color: '000000',
  bg: 'ffffff',
  format: 'svg' // 또는 'png'
});

const res = await fetch(`${botariData.root}botari/v1/qr?${params.toString()}`, {
  method: 'GET',
  headers: { 'X-WP-Nonce': botariData.nonce },
});
const json = await res.json();
// format=svg: json.svg (string), 예: '<svg ...>'
// format=png: json.png (string), 예: 'data:image/png;base64,...'
```

**응답 예시**
```json
{
  "format": "svg",
  "svg": "<svg ...></svg>",
  "data": "https://botari.net/app/123",
  "size": 256,
  "margin": 2,
  "color": "#000000",
  "bg": "#ffffff",
  "cached": false
}
```

```json
{
  "format": "png",
  "png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
  "data": "https://botari.net/app/123",
  "size": 256,
  "margin": 2,
  "color": "#000000",
  "bg": "#ffffff",
  "cached": false
}
```

## 5.2 바코드 생성 API

외부 보따리앱에서 바코드 생성 앱을 만들 때 사용합니다. SVG는 서버에서 바로 생성하며
별도 외부 라이브러리나 node 런타임에 의존하지 않습니다.

**Endpoint**
- `GET /wp-json/botari/v1/barcode`

**필수 파라미터**
- `value` (string): 바코드에 넣을 값

**선택 파라미터**
- `type` (`code128` or `ean13`, default: `code128`)
- `format` (`svg`, default: `svg`)
- `height` (40~400, default: 120)
- `scale` (1~8, default: 2)
- `margin` (0~80, default: 10)
- `color` (HEX without #, default: 111111)
- `bg` (HEX without #, default: ffffff)
- `text` (`1` or `0`, default: `1`)

**SDK 예시**
```js
const barcode = await BotariSDK.generateBarcode({
  type: 'code128',
  value: 'BOTARI-12345',
  height: 120,
  scale: 2
});

document.querySelector('#barcode').innerHTML = barcode.svg;
```

**REST 예시**
```js
const params = new URLSearchParams({
  type: 'ean13',
  value: '8801234567893',
  format: 'svg'
});
const res = await fetch(`/wp-json/botari/v1/barcode?${params.toString()}`);
const json = await res.json();
```

**응답 예시**
```json
{
  "format": "svg",
  "type": "code128",
  "value": "BOTARI-12345",
  "svg": "<svg ...></svg>",
  "width": 332,
  "height": 120
}
```

## 5.3 휴대폰 소유자 인증 API

조문, 방명록, 예약, 문의처럼 보따리앱 안에서 특정 행동 전에 휴대폰 번호를 실제로 수신 가능한지 확인할 때 사용합니다. 이 기능은 휴대폰 소유자 인증 또는 문자 인증이며, 실명/명의/성인 본인인증이 아닙니다. UI와 문서에서도 본인인증, 실명인증, 성인인증, 명의확인이라고 표현하면 안 됩니다.

**Endpoint**
- `POST /wp-json/botari/v1/sms/send-code`
- `POST /wp-json/botari/v1/sms/verify-code`

**AI/앱 제작자 주의**
- 보따리앱 안에서는 REST 후보를 자동 탐색하지 말고 `BotariSDK.sendSmsCode()`와 `BotariSDK.verifySmsCode()`를 사용합니다.
- 이 API는 보따리 회원가입을 요구하지 않습니다. 허용된 보따리앱에서 휴대폰 소유자 인증만 통과하면 됩니다.
- `verified` 결과는 해당 앱의 특정 행동 허용에만 사용하고, 실명/성인/명의 판단에는 사용하지 않습니다.

**관리 위치**
- 관리자: `보따리 관리 > 시스템 > 문자 인증`
- 허용 제품 ID와 발송 제한은 관리자 화면에서 관리
- Solapi 키/시크릿/발신번호는 서버 환경변수로만 관리

**사용 조건**
- 서버에 `SOLAPI_API_KEY`, `SOLAPI_API_SECRET`, `SMS_FROM` 설정 필요
- `BOTARI_SMS_ALLOWED_PRODUCTS` 또는 `botari_sms_allowed_products` 옵션으로 허용된 앱만 발송 가능
- 기본 제한: 같은 번호 하루 5회, 같은 IP 하루 10회, 앱별 하루 100회, 재발송 60초 제한

**SDK 시그니처**
```js
await BotariSDK.sendSmsCode({
  phone: '01012345678',
  purpose: 'condolence_message', // optional, default: 'default'
  productId: 123 // optional, 없으면 현재 실행 앱 ID 자동 감지
});

await BotariSDK.verifySmsCode({
  phone: '01012345678',
  code: '123456',
  purpose: 'condolence_message',
  productId: 123
});
```

**sendSmsCode 응답**
```json
{
  "success": true,
  "expires_in": 300,
  "retry_after": 60,
  "phone_last4": "5678"
}
```

**verifySmsCode 응답**
```json
{
  "success": true,
  "verified": true,
  "verified_for": 1800,
  "phone_last4": "5678"
}
```

**SDK 예시**
```js
await BotariSDK.sendSmsCode({
  phone: '01012345678',
  purpose: 'condolence_message'
});

const result = await BotariSDK.verifySmsCode({
  phone: '01012345678',
  code: '123456',
  purpose: 'condolence_message'
});

if (result.verified) {
  // 조문 등록 진행
}
```

## 6. 에러 처리

- `rest_not_logged_in` / 권한 오류: 로그인 필요
- `login_required`: SDK에서 QR/로그인 필요 응답을 표준화한 오류
- `not_allowed`: 권한/정책 위반
- `storage_forbidden`: 앱 스토리지 접근 권한 없음
- `invalid_storage_key`: 앱 스토리지 key 형식 오류
- `missing_value`: 앱 스토리지 저장 요청에 `value` 누락
- `storage_value_too_large`: 단일 저장값 64KB 초과
- `storage_quota_exceeded`: 사용자-앱별 총 5MB 초과
- `missing_data`: QR data 누락
- `invalid_format`: `format` 값이 지원 범위 밖인 경우
- `qr_unavailable`, `qr_failed`, `qr_png_unsupported`: QR 생성 실패
- `qr_data_too_long`: 내부 QR SVG 생성 범위를 초과한 데이터
- `invalid_barcode_type`: 바코드 type이 지원 범위 밖인 경우
- `invalid_barcode_value`, `invalid_barcode_checksum`, `barcode_value_too_long`: 바코드 값 검증 실패
- `sms_not_configured`, `sms_not_allowed`, `sms_resend_wait`, `sms_code_expired`, `invalid_sms_code`: 휴대폰 소유자 인증/SMS 문자 인증 오류

문서가 계속 비어 보이면 운영 페이지 `/docs` 에서 `botari_doc` 값이
`api-guide`인지 확인해주세요.