보따리
Docs

AI API Quick Reference

# Botari AI API Quick Reference

이 문서는 AI 앱 제작자가 보따리 서버 기능을 빠르게 찾기 위한 공개 인덱스입니다. 최근 기능만 보지 말고, 아래 전체 기능 목록에서 필요한 API를 먼저 확인하세요.

## 먼저 읽을 순서

1. AI 워크스페이스 작업 규칙: `https://botari.net/docs/ai-workspace.md`
2. AI용 전체 API 요약: `https://botari.net/docs/ai-api.md`
3. OCR 전용 짧은 문서: `https://botari.net/docs/ocr-api.md`
4. 전체 상세 API Guide: `https://botari.net/docs/api-guide.md`
5. API 호스트 mirror: `https://api.botari.net/docs/ai-api.md`

## 공통 원칙

- REST root: `https://api.botari.net/wp-json/`
- Botari route prefix: `/wp-json/botari/v1/...`
- 보따리앱 iframe 안에서는 REST 경로를 추측하지 말고 `BotariSDK`를 우선 사용합니다.
- 앱 ZIP 안에서 `/wp-json/botari/v1/*` 상대 경로를 직접 호출하지 마세요. 실행 도메인이 `play.botari.net`일 수 있습니다.
- `wpApiSettings`, WordPress nonce, WordPress 쿠키에 직접 의존하지 마세요.
- SDK는 실행 중인 앱의 식별 정보, 실행 세션, 권한 확인을 자동 처리합니다.
- 앱 ZIP 안에 fake/noop `BotariSDK`를 넣지 마세요. 로컬 fallback은 `window.BotariSDK`가 없을 때만 둡니다.

## 현재 사용 가능한 주요 SDK

아래 기능은 예정이 아니라 현재 보따리앱에서 사용할 수 있는 SDK입니다.

- 같은 앱 사용자에게 보이는 모집/대기/방명록/투표 목록: `addSharedItem()`, `listSharedItems()`, `updateSharedItem()`, `deleteSharedItem()`
- 같은 게임의 자동 상대 찾기: `findMatch()`, `requestMatch()`, `cancelMatch()`
- 같은 방 안의 실시간 이벤트와 참가자 목록: `joinRoom()`
- 현재 로그인 사용자 공개 표시 정보: `getCurrentUser()`
- 일반 콜택시 번호/지역 수집 상태: `getNearbyCallTaxis()`, `getCallTaxiCoverage()`
- 사용자별 앱 데이터 저장: `setStorage()`, `getStorage()`, `listStorage()`, `deleteStorage()`
- 게임 점수/순위: `submitScore()`, `getRankings()`

예: 바둑/체스/보드게임의 자동 상대 찾기는 `findMatch('ranked')`를 사용하고, 매칭 응답의 `room_id`로 `joinRoom()`에 입장합니다. 사용자가 직접 상대를 고르는 모집 목록만 Shared Board/List를 사용합니다. 전체 방 목록 API를 찾거나 직접 만들지 마세요.

## 전체 기능 인덱스

| 영역 | SDK 우선 사용 | REST/서버 경로 | 용도 |
|---|---|---|---|
| 앱 실행 기록 | `recordPlay()` | `POST /botari/v1/game/{id}/play` | 실행/플레이 카운트 기록 |
| 게임 점수 | `submitScore(score, meta?)` | `POST /botari/v1/game/{id}/score` | 게임 점수 제출 |
| 게임 순위 | `getRankings(options?)` | `GET /botari/v1/game/{id}/rankings` | 랭킹 조회 |
| 내 최고점 | SDK 내부/REST fallback | `GET /botari/v1/game/{id}/my-best-score` | 로그인 사용자의 최고 점수 |
| 앱 행동 이벤트 | `trackEvent(name, props?)` | SDK 내부 처리 | QR 생성, OCR 실패, 다운로드 등 앱별 이벤트 |
| JSON Storage | `listStorage()`, `getStorage()`, `setStorage()`, `deleteStorage()` | SDK 우선 | 앱별 사용자 데이터, 설정, 초안, 템플릿 |
| Shared Board/List | `addSharedItem()`, `listSharedItems()`, `updateSharedItem()`, `deleteSharedItem()` | SDK 우선 | 같은 앱 사용자에게 보이는 모집/투표/방명록/현황 목록 |
| Automatic Matchmaking | `findMatch()`, `requestMatch()`, `cancelMatch()` | SDK 우선 | 같은 게임·모드·배포 버전 사용자 2명을 원자적으로 매칭 |
| Realtime Room | `joinRoom(roomId, options?)` | SDK 우선 | 소규모 게임/협업/투표용 실시간 이벤트 동기화 |
| 현재 사용자 표시 | `getCurrentUser()` | SDK 우선 | 공개 표시 이름·아바타·좌판 URL과 로그인 여부 확인 |
| QR 생성 | `generateQR(payload)` | `GET /botari/v1/qr` | QR SVG/PNG 생성 |
| 바코드 생성 | `generateBarcode(payload)` | `GET /botari/v1/barcode` | code128 등 바코드 생성 |
| SMS 소유자 확인 | `sendSmsCode()`, `verifySmsCode()` | `/botari/v1/sms/send-code`, `/sms/verify-code` | 휴대폰 번호 수신 가능 여부 확인. 실명/본인/성인 인증 아님 |
| OCR | `extractTextFromImage()`, `createOcrJob()`, `getOcrJob()` | `/ocr/image`, `/ocr/jobs/{job_id}` | Gemini 우선 이미지 텍스트 추출, 실패 시 PaddleOCR fallback |
| 식품 라벨 OCR 정리 | `extractTextFromImage({ normalize_label_ocr:true })`, `normalizeLabelOcr()` | `POST /ocr/normalize-label` | Gemini 우선 OCR+`fields` JSON 동시 정리, fallback 결과는 별도 정리 |
| Excel 생성 | `generateExcel()`, `createExcel()` | `POST /document/excel` | 서버에서 `.xlsx` 생성 |
| PDF 생성 | `generatePdf()`, `createPdf()` | `POST /document/pdf` | 서버 이미지 기반 PDF 생성 |
| 벡터 PDF 생성 | `generateVectorPdf()`, `createVectorPdf()` | `POST /document/vector-pdf` | 선택 가능한 텍스트 PDF 생성 |
| 현재 위치 요청 | `requestLocation({ purpose })` | parent bridge | 앱 실행 중 별도 고지/동의 후 현재 브라우저 위치만 반환 |
| 지도 위치 보정 | `openLocationPicker({ latitude, longitude, purpose })` | SDK 우선 | 보따리 지도에서 중앙 핀 위치를 선택하고 좌표만 반환. 서버 저장 없음 |
| 지역 콜택시 번호 | `getNearbyCallTaxis()`, `getCallTaxiCoverage()` | SDK 우선 | 동의받은 현재 좌표 또는 행정구역의 일반 전화 호출 번호와 수집 상태 |
| 보따리앱 PWA 모드 | `openInstallGuide()` + 브라우저 표준 `manifest.json`, `service-worker.js` | 보따리앱 ZIP 업로드/실행, `/pwa-install-guide/` | 홈 화면 추가 안내, standalone 실행, 정적 자원 캐시, 오프라인 UX |
| 보따리앱 사용자 설정 | `getSettings()` + ZIP 내부 `settings.json` | `/app-settings/{product_id}` | 보따리가 자동 설정 폼 제공, 앱은 사용자별 설정값만 읽음 |
| 지역 콜택시 번호 | `getNearbyCallTaxis()`, `getCallTaxiCoverage()` | `GET /botari/v1/map/call-taxis/nearby`, `/coverage` | 현재 좌표 또는 행정구역의 일반 전화 호출 번호와 수집 가능 상태 |

## 게임 / 순위 / 앱 이벤트

### SDK

```js
await BotariSDK.recordPlay();
await BotariSDK.submitScore(1200, { level: 3, mode: 'normal' });
const rankings = await BotariSDK.getRankings({ limit: 10 });
await BotariSDK.trackEvent('stage_cleared', { stage: 3 });
```

### REST

- `POST /wp-json/botari/v1/game/{id}/play`
- `POST /wp-json/botari/v1/game/{id}/score`
- `GET /wp-json/botari/v1/game/{id}/rankings?limit=10`
- `GET /wp-json/botari/v1/game/{id}/my-best-score`

`trackEvent()`는 게임 외 앱에서도 씁니다. 예: `qr_generated`, `barcode_created`, `ocr_started`, `ocr_failed`, `png_downloaded`, `storage_save_failed`.

로그 사용 제한:
- 이벤트 이름은 `qr_generated`, `pdf_generated`, `ocr_failed`처럼 짧은 영문/숫자/밑줄 형태로 작성합니다.
- `props`는 상태·형식·개수 같은 짧은 값만 보냅니다. 예: `{ format: 'pdf', page_count: 1, reason: 'timeout' }`
- 이름, 전화번호, 이메일, 주소, 위치 좌표, OCR 원문, 라벨 본문, 프롬프트, 이미지/base64, 파일 내용, 토큰/비밀번호/쿠키는 보내지 않습니다.
- 서버는 앱 실행 권한을 확인하고, 짧은 상태 정보만 기록합니다.
- 민감 키나 민감해 보이는 값은 저장하지 않을 수 있으며, 반복 전송은 제한될 수 있습니다.

## JSON Storage

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

초기 화면은 저장소 응답을 기다리지 말고 먼저 렌더링하세요. `listStorage()`와
`getStorage()`는 일시적인 네트워크/릴레이 장애일 때 앱 실행을 막지 않도록
`unavailable: true`가 붙은 빈 응답을 반환할 수 있습니다. `setStorage()`와
`deleteStorage()` 실패는 실제 저장 실패이므로 앱에서 직접 안내해야 합니다.

앱 실행 중에는 REST 경로를 직접 호출하지 말고 SDK만 사용하세요.

제한:

- key 최대 100자
- 단일 value 최대 64KB
- JSON Storage 사용자-앱별 총량 제한이 있음 (앱 ZIP 업로드 한도가 아님)
- `storage_quota_exceeded`가 나올 수 있음

## Shared Board/List

같은 앱을 실행 중인 로그인 사용자끼리 짧은 공개 항목 목록을 공유할 때 사용합니다. 예: “함께해요” 모집, 방명록, 예약 현황, 투표 옵션, 대기열, 공동 위시리스트.

SDK만 사용하세요. 직접 REST 경로를 추측하지 마세요.

```js
await BotariSDK.addSharedItem('together', {
  message: '함께해요',
  roomId: 'room-1'
}, { expiresIn: 1800 });

const list = await BotariSDK.listSharedItems('together', { limit: 50 });

await BotariSDK.updateSharedItem(list.items[0].id, {
  message: '모집이 마감되었습니다',
  roomId: 'room-1',
  status: 'closed'
}, { expiresIn: 600 });

await BotariSDK.deleteSharedItem(list.items[0].id);
```

정책:

- `boardKey`는 영문/숫자/`.`, `_`, `:`, `-` 1~64자입니다. 예: `together`, `votes`, `guestbook`.
- 항목은 같은 앱과 같은 `boardKey` 안에서만 조회됩니다.
- 로그인된 앱 실행 세션에서만 사용할 수 있습니다. 비로그인 공개 실행에서는 차단됩니다.
- 항목은 일정 시간이 지나면 만료되며, 앱은 `expiresIn`으로 원하는 유지 시간을 요청할 수 있습니다.
- 목록 조회는 최신순이며 `limit`으로 표시 개수를 조절할 수 있습니다.
- 수정과 삭제는 본인이 등록한 활성 항목만 가능합니다. 수정으로 앱, `boardKey`, 작성자는 바꿀 수 없습니다.
- 응답 사용자 정보는 표시 이름과 avatar URL 같은 공개 표시 정보만 포함합니다.
- 실시간 이벤트가 필요하면 `joinRoom()`을 함께 쓰고, 방 발견/모집 목록은 `listSharedItems()`로 처리하세요.

## Automatic Matchmaking API

현재 사용 가능한 로그인 사용자용 SDK입니다. 같은 게임에서 같은 `queueKey`를 사용한 두 사용자를 서버가 자동으로 짝지어 동일한 `match_id`와 `room_id`를 반환합니다.

```js
const match = await BotariSDK.findMatch('ranked-duel', {
  timeout: 30000,
  pollInterval: 1500,
  expiresIn: 60
});

if (match.status === 'matched') {
  const room = await BotariSDK.joinRoom(match.room_id, { create: true });
  startGame({
    room,
    playerNumber: match.player_number,
    opponent: match.opponent
  });
}
```

- `findMatch()`는 매칭될 때까지 자동으로 확인하고, 제한 시간이 지나면 `status: "waiting"`을 반환합니다. 같은 큐로 다시 호출하면 기존 검색을 이어갑니다.
- 한 번만 상태를 확인하려면 `requestMatch()`를 사용합니다.
- 사용자가 찾기를 중단하면 `cancelMatch(queueKey)`를 호출합니다.
- `queueKey`를 `ranked`, `casual`, `two-player`처럼 모드별로 나누면 서로 다른 모드 사용자가 섞이지 않습니다.
- 테스트모드와 공개 실행, 서로 다른 앱 버전은 자동으로 분리됩니다.
- 매칭 응답의 `player_number`는 1 또는 2이며, `is_host`는 1P에게만 `true`입니다.
- 상대에게는 공개 식별값, 표시 이름, 프로필 이미지만 전달됩니다.
- 실제 게임 이벤트는 반드시 응답의 `room_id`로 `joinRoom()`에 입장한 뒤 주고받습니다.
- 대기 목록을 직접 보여주고 사용자가 상대를 고르게 할 때만 Shared Board/List를 사용합니다.

## Realtime Room API

운영 연결은 `BotariSDK.joinRoom()`만 사용하고 실시간 연결을 직접 만들지 마세요.

목적:

- 채팅 API가 아니라 보따리앱의 가벼운 실시간 이벤트 동기화 API입니다.
- 대상: 턴제 게임, 퀴즈, 보드게임, 실시간 투표, 교육용 화면 동기화, 협업 도구.
- 자유 텍스트 채팅은 1차 범위가 아닙니다. 게임 중 대화는 앱이 미리 정의한 `quick_chat`/이모트 이벤트를 권장합니다.

SDK 사용:

```js
const room = await BotariSDK.joinRoom('race-abc123', { create: true });

room.onPeerJoin((peer) => {});
room.onPeerLeave((peer) => {});
room.onMessage((from, payload) => {});

room.send({ type: 'ready', value: true });
room.send({ type: 'quick_chat', value: 'again' });

room.leave();
```

반환:

```js
{
  room_id: 'race-abc123',
  me: { peer_id: 'p_x7df9', nickname: 'onesun' },
  peers: [
    { peer_id: 'p_ab12', nickname: 'josh' }
  ]
}
```

사용 규칙:

- 외부 실시간 연결이나 내부 URL을 직접 연결하지 말고 `BotariSDK.joinRoom()`만 사용하세요.
- room id는 앱이 직접 정합니다. 같은 앱 안에서만 의미가 있습니다.
- 레이싱, 실시간 스케치, 추격전처럼 고빈도 동기화가 필요한 게임은 상품이 게임용 realtime profile로 설정된 경우 더 높은 전송 빈도를 사용할 수 있습니다.
- 고빈도 게임에서는 매 프레임 전송하지 말고 50ms 단위로 입력/위치/속도만 묶어 보내고, 수신 쪽은 보간하세요.
- `room.peers`, `onPeerJoin()`, `onPeerLeave()`는 같은 room 안의 참가자만 보여줍니다. 같은 게임 전체 접속자 목록이 필요하면 Shared Board/List를 사용하세요.
- 메시지는 짧은 JSON 이벤트로 보내세요. 긴 텍스트, 파일, 개인정보는 보내지 않습니다.
- 방 발견/모집/대기 목록은 `addSharedItem()`/`listSharedItems()`나 공유 링크/QR로 처리하세요.
- 연결 실패, 방 없음, 인원 초과, 전송 제한 같은 오류가 날 수 있으므로 앱에서 재시도/나가기 UI를 준비하세요.
- 영속 상태 복구가 필요하면 JSON Storage를 함께 사용하세요.

에러 코드:

- `login_required`
- `room_full`
- `room_not_found`
- `message_too_large`
- `rate_limited`
- `too_many_rooms`
- `realtime_token_expired`
- `service_busy`

미니 예제: 5인 가위바위보

```js
const room = await BotariSDK.joinRoom('rps-' + location.hash.slice(1), { create: true });
const choices = {};

room.onMessage((from, msg) => {
  if (msg.type !== 'pick') return;
  choices[from.peer_id] = msg.value;
  renderChoices(choices);
});

document.querySelectorAll('[data-pick]').forEach((button) => {
  button.onclick = () => {
    room.send({ type: 'pick', value: button.dataset.pick });
  };
});
```

## OCR / 식품 라벨 OCR

```js
const result = await BotariSDK.extractTextFromImage({
  image_base64: imageBase64,
  mime: 'image/jpeg',
  timeout_ms: 120000,
  poll_interval_ms: 1500,
  normalize_label_ocr: true
});

console.log(result.clean_text);
console.log(result.fields);
```

OCR 실패는 앱에서 감지해야 합니다. `extractTextFromImage()` 실패 시
`code`, `user_message`, `retryable`, `stage`, `job_id`가 있는 에러가 throw됩니다.
화면에는 `user_message`를 보여주고, `retryable`이 참일 때만 재시도 버튼을 노출하세요.

```js
try {
  const result = await BotariSDK.extractTextFromImage({
    image_base64: imageBase64,
    mime: 'image/jpeg'
  });
  showOcrResult(result);
} catch (error) {
  showError(error.user_message || 'OCR에 실패했습니다.');
  await BotariSDK.trackEvent('ocr_failed', {
    code: error.code,
    stage: error.stage,
    retryable: !!error.retryable
  });
}
```

중요:

- `normalize_label_ocr: true`를 쓰면 Gemini가 이미지 OCR과 식품 라벨 JSON 정리를 한 번에 시도합니다. Gemini가 실패해 PaddleOCR로 fallback된 경우에만 SDK가 `normalize-label`을 추가 호출합니다.
- 같은 결과로 `BotariSDK.normalizeLabelOcr()`를 다시 호출하지 마세요. fallback이 아닌 정상 Gemini 경로에서는 이미 `label_ocr`가 포함됩니다.
- 직접 후처리는 이미 OCR 결과가 있을 때만 사용합니다.

```js
const normalized = await BotariSDK.normalizeLabelOcr({
  text: ocr.text,
  lines: ocr.lines
});
```

REST:

- `POST /wp-json/botari/v1/ocr/image`
- `GET /wp-json/botari/v1/ocr/jobs/{job_id}`
- `POST /wp-json/botari/v1/ocr/normalize-label`

제한:

- 로그인 사용자만 사용 가능
- jpg/png/webp 지원
- 이미지 3MB 이하
- 사용자당 활성 OCR 작업 5개
- 원본 이미지는 처리 완료/실패 후 삭제
- 완료/실패 결과는 24시간 보관 후 정리

## QR / Barcode

```js
const qr = await BotariSDK.generateQR({
  data: 'https://botari.net/app/123',
  size: 256,
  format: 'svg'
});

const barcode = await BotariSDK.generateBarcode({
  type: 'code128',
  value: 'BOTARI-12345',
  height: 120
});
```

REST:

- `GET /wp-json/botari/v1/qr`
- `GET /wp-json/botari/v1/barcode`

## SMS phone ownership check

이 기능은 휴대폰 번호로 SMS를 받을 수 있는지 확인하는 기능입니다. 실명 인증, 법적 본인 인증, 성인 인증으로 설명하지 마세요.

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

REST:

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

## Document generation

```js
const excel = await BotariSDK.generateExcel({
  filename: 'labels.xlsx',
  sheets: [{ name: 'Labels', rows: [['제품명', '원재료'], ['고추장', '정제수']] }]
});

const pdf = await BotariSDK.generateVectorPdf({
  filename: 'label.pdf',
  title: '제품 라벨',
  elements: [{ type: 'text', text: '고추장', x: 56, y: 80, size: 18 }]
});
```

REST:

- `POST /wp-json/botari/v1/document/excel`
- `POST /wp-json/botari/v1/document/pdf`
- `POST /wp-json/botari/v1/document/vector-pdf`

## 위치 API

```js
const location = await BotariSDK.requestLocation({
  purpose: '내 주변 장소를 찾기 위해 현재 위치가 필요합니다.',
  enableHighAccuracy: true,
  timeoutMs: 15000
});
```

정책:

- 개별 보따리앱은 보따리 계정에 저장된 회원 위치정보를 직접 조회할 수 없습니다.
- 앱 실행 중 사용자에게 별도 목적 고지와 동의를 받은 현재 브라우저 위치만 반환합니다.
- 위치 기능을 쓰는 앱은 앱 화면에서 별도 동의를 받아야 합니다.

현재 좌표를 지도에서 직접 보정해야 하면 보따리 지도 선택기를 사용합니다.

```js
const corrected = await BotariSDK.openLocationPicker({
  latitude: location.latitude,
  longitude: location.longitude,
  purpose: '콜택시를 부를 위치를 조정합니다.'
});
```

- 사용자는 화면 중앙 핀에 맞춰 지도를 움직인 뒤 `이 위치로 사용`을 누릅니다.
- 반환값은 `latitude`, `longitude`뿐이며 선택 좌표는 서버에 저장되지 않습니다.
- 취소하면 `location_picker_cancelled` 오류로 종료됩니다.
- 앱 ZIP에 외부 지도 SDK나 지도 API 키를 직접 넣지 마세요.

## 지역 콜택시 번호 API

현재 위치 또는 이미 알고 있는 행정구역으로 일반 전화 호출 번호를 찾습니다. 보따리앱은 REST 경로를 직접 만들지 않고 SDK를 사용합니다.

```js
const location = await BotariSDK.requestLocation({
  purpose: '주변에서 전화로 부를 수 있는 택시를 찾기 위해 현재 위치가 필요합니다.'
});

const result = await BotariSDK.getNearbyCallTaxis({
  latitude: location.latitude,
  longitude: location.longitude
});

const coverage = await BotariSDK.getCallTaxiCoverage({
  sido: result.region.sido,
  status: 'available'
});
```

- `available=true`: 현재 누를 수 있는 일반 호출번호가 있음
- `coverage_status`: `available`, `unavailable`, `collecting`, `unverified`
- `data_status`: `ready`, `collecting`, `missing`
- 미수집 지역은 `unverified`이며 `unavailable`로 표시하지 않습니다.
- `items[].tel_url`을 전화 버튼 링크로 사용할 수 있습니다.
- `items[].id`는 해당 번호의 오류 신고에 사용하는 숫자 ID입니다.
- `pickup_context.road_address`, `lot_address`는 현재 좌표의 도로명주소와 지번주소입니다. 앱이 목적에 맞는 값을 선택합니다.
- `pickup_context.landmarks`에는 가까운 역·터미널·병원·공공시설·대형 상업시설 후보가 거리순으로 최대 10개 들어갑니다. 서버는 대표 시설을 지정하지 않습니다.
- 호환성을 위해 `primary_landmark`는 `null`, `suggested_phrase`는 주소만 담아 반환합니다. 앱은 주소·지번·주변 후보 중 사용자에게 보여줄 내용을 직접 선택해야 합니다.
- 랜드마크 조회가 실패해 `landmark_status=missing`이어도 주소와 전화번호는 계속 사용합니다. 행정구역만 보낸 경우 상태는 `coordinates_required`입니다.
- 전체 현황은 `BotariSDK.getCallTaxiCoverage()`로 조회합니다. 보따리앱 밖의 외부 클라이언트만 `https://api.botari.net/wp-json/botari/v1/map/call-taxis/*` 절대 URL을 사용합니다.
- 번호가 틀렸거나 연결되지 않으면 `POST https://api.botari.net/wp-json/botari/v1/map/call-taxis/report`에 `taxi_id`, `reason`, 선택 `details`를 JSON으로 보낼 수 있습니다. `reason`은 `wrong_number`, `disconnected`, `service_ended`, `wrong_area`, `other` 중 하나입니다.
- 같은 사용자의 같은 번호 반복 신고는 제한됩니다. 신고 기능은 통화 실패 뒤 사용자가 직접 선택했을 때만 보여주세요.
- 장애인·교통약자 전용 번호는 일반 결과에 포함되지 않습니다.
- 지정 마을 주민만 이용하는 행복·희망·마을택시도 현재 일반 결과에 섞지 않습니다. `coverage_status=unverified` 또는 `collecting`인 지역에 임의의 민간 번호를 대신 표시하지 마세요.

## Protected app context

일반 앱 제작자는 내부 중계 경로를 직접 호출하지 말고 SDK 메서드를 사용하세요. SDK가 실행 중인 앱의 권한과 컨텍스트를 처리합니다.

## 보따리앱 PWA 모드 / 홈 화면 추가 지원

보따리는 별도 PWA 앱 타입 대신 보따리앱의 PWA 모드를 지원합니다. AI가 설치 가능한 앱을 만들 때도 타입은 보따리앱으로 두고, ZIP 루트에 필요한 정적 파일을 포함하는 방식을 우선 사용하세요.

보따리는 앱별 설치 manifest를 실행 페이지에 자동으로 연결합니다. 사용자가 홈 화면에 추가하면 보따리 전체가 아니라 해당 보따리앱 실행 URL(`/bottariapp/{product_id}`)이 standalone으로 열립니다.
설치된 앱이 standalone 모드로 실행될 때는 보따리 실행 상단 바가 숨겨지고 앱 화면만 표시됩니다.

권장 ZIP 구조:

```text
my-pwa/
├── index.html
├── app.js
├── style.css
├── manifest.json
├── service-worker.js
├── icon-192.png
└── icon-512.png
```

필수/권장 기준:

- `index.html`은 ZIP 루트의 진입 파일입니다.
- `manifest.json`에는 `name`, `short_name`, `start_url`, `display`, `theme_color`, `background_color`, `icons`를 넣습니다.
- 앱 이름, 홈 화면 이름, 테마 색상, 배경 색상은 ZIP 안의 실제 PWA `manifest.json` 값을 우선 사용합니다.
- 등록 화면의 PWA 색상 설정은 선택적 덮어쓰기/폴백입니다. AI가 앱을 만들 때는 색상을 등록 필드에 맡기지 말고 `manifest.json`에 직접 넣으세요.
- `display`는 보통 `standalone`을 사용합니다.
- 아이콘은 앱 고유 아이콘으로 192x192 PNG와 512x512 PNG를 준비합니다. 보따리 로고를 원본 아이콘 안에 직접 넣지 마세요. 플랫폼이 홈 화면용 아이콘을 만들 때 우하단에 작은 보따리 배지를 자동 합성합니다.
- `icons[].src`는 ZIP 내부 상대 경로를 사용합니다. 예: `./icon-192.png`, `./icon-512.png`. 빌드 과정에서 `assets/icon-512.{hash}.png`처럼 파일명이 바뀌어도 플랫폼이 실제 PWA manifest 위치를 기준으로 찾아 연결합니다.
- `manifest.json`은 앱 설치용 Web App Manifest여야 합니다. 빌드 메타데이터, 번들 manifest, Vite manifest를 앱 PWA manifest처럼 만들지 마세요.
- `service-worker.js`로 HTML/CSS/JS/이미지 같은 정적 자원을 캐시할 수 있습니다.
- 앱 화면은 모바일 320px 이상에서 동작해야 합니다.
- 서버 기능은 PWA에서도 동일하게 `BotariSDK`를 우선 사용합니다.
- 앱 안에서 설치 안내를 열 때는 `BotariSDK.openInstallGuide()`를 사용합니다.
- 제작자가 `manifest.json` 또는 `service-worker.js`를 빠뜨린 경우 PWA 모드가 켜진 보따리앱은 앱 이름, 아이콘, 선택적 등록 색상 또는 기본 색상을 바탕으로 기본 파일을 생성합니다. 다만 AI가 직접 생성할 수 있다면 앱에 맞는 파일을 ZIP 안에 포함하는 편이 가장 좋습니다.
- 푸시 알림 권한은 플랫폼의 설치 안내/권한 모달이 관리합니다. 앱이 시작하자마자 직접 알림 권한창을 띄우지 마세요.
- 사용자가 권한을 허용하지 않아도 사이트 내부 알림은 받을 수 있습니다. 푸시 알림은 내부 알림의 보조 전달 수단입니다.

PWA 예시 manifest:

```json
{
  "name": "My Botari PWA",
  "short_name": "BotariPWA",
  "start_url": "./index.html",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#f8384b",
  "icons": [
    {
      "src": "./icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "./icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}
```

서비스워커 주의:

- `service-worker.js`는 앱 정적 파일 캐시와 오프라인 안내 용도로 사용합니다.
- `/wp-json/botari/v1/*`, 결제, OCR, SMS, 저장소 같은 서버 API 응답을 무리하게 영구 캐시하지 마세요.
- 보따리 실행 도메인은 플랫폼이 관리하므로 절대 경로보다 ZIP 내부 상대 경로를 우선 사용하세요.
- 새 버전 배포 시 캐시 이름을 바꿔 이전 자원이 남지 않게 하세요.

앱 안 설치 안내 버튼 예시:

```html
<button id="install-help">홈 화면에 설치하기</button>
<script>
document.getElementById('install-help').addEventListener('click', async () => {
  if (!window.BotariSDK || !BotariSDK.openInstallGuide) {
    window.open('https://botari.net/pwa-install-guide/', '_blank', 'noopener');
    return;
  }
  await BotariSDK.openInstallGuide();
});
</script>
```

사용자용 상세 안내 페이지:

- `https://botari.net/pwa-install-guide/`

## 보따리앱 사용자 설정

앱별 사용자화가 필요하면 앱 안에 별도 설정 페이지를 만들지 말고 ZIP 안에 `settings.json`을 넣으세요. 보따리가 내 보따리에서 `내 설정` 폼을 자동으로 만들고, 앱은 실행 중 `BotariSDK.getSettings()`로 저장된 사용자별 값을 읽습니다.

`settings.json`이 없으면 보따리는 설정 버튼을 표시하지 않고 앱은 기존처럼 실행됩니다. 원본 ZIP은 수정하지 않으며 사용자별 설정값만 별도로 저장됩니다.

예시:

```json
{
  "version": 1,
  "groups": [
    {
      "key": "basic",
      "label": "기본",
      "fields": [
        { "key": "title", "label": "앱 제목", "type": "text", "default": "나의 앱" },
        { "key": "themeColor", "label": "테마 색상", "type": "color", "default": "#f8384b" },
        { "key": "showCompleted", "label": "완료 항목 표시", "type": "boolean", "default": true }
      ]
    }
  ]
}
```

지원 타입:

- `text`, `textarea`, `number`, `boolean`, `color`, `select`, `image`, `url`

앱 코드:

```js
const settings = await BotariSDK.getSettings();

document.title = settings.title || '나의 앱';
document.documentElement.style.setProperty('--theme-color', settings.themeColor || '#f8384b');
```

## 보따리앱 ZIP 업로드 제한

주의: 위 JSON Storage의 사용자-앱별 총 10MB는 저장소 quota입니다. 보따리앱 ZIP 업로드 한도는 아래 별도 기준을 사용합니다.

- ZIP 원본 파일: 30MB
- ZIP 내부 파일 수: 500개
- 압축 해제 후 총량: 30MB
- ZIP 내부 단일 파일: 10MB
- 5MB 제한은 보따리앱 ZIP 한도가 아니라 댓글 이미지/일부 이미지 업로드 또는 앱 JSON Storage 총량 제한입니다.

## 자주 나오는 에러

- `login_required`: 로그인이 필요함
- `library_required`: 해당 보따리템 접근 권한이 없음
- `storage_quota_exceeded`: 앱 저장소 용량 초과
- `ocr_image_too_large`: OCR 이미지 3MB 초과
- `ocr_invalid_mime`: OCR 미지원 이미지 형식
- `ocr_queue_limit`: 활성 OCR 작업이 너무 많음
- `ocr_timeout`: 클라이언트 폴링 시간 초과
- `ocr_service_unavailable`: 내부 OCR 서비스 장애
- `ocr_normalize_failed`: Gemini 라벨 정리 실패 또는 quota/rate limit

## AI 앱 제작 체크리스트

- `BotariSDK`를 먼저 사용한다.
- 앱 ZIP 안에 fake/noop SDK를 넣지 않는다.
- `/wp-json/botari/v1/*` 상대 REST 경로를 추측 호출하지 않는다.
- WordPress nonce나 쿠키에 직접 의존하지 않는다.
- 게임이면 `recordPlay()`, `submitScore()`, `getRankings()`를 확인한다.
- 저장이 필요하면 JSON Storage를 사용하고 10MB 총량을 고려한다.
- “함께해요”, 방명록, 예약 현황, 투표 목록처럼 같은 앱 사용자에게 보이는 목록은 Shared Board/List `addSharedItem()`/`listSharedItems()`/`updateSharedItem()`/`deleteSharedItem()`을 사용한다.
- 자동 온라인 대전은 `findMatch()`로 상대를 찾고, 응답의 `room_id`로 `joinRoom()`에 입장한다. 사용자 선택형 모집 목록만 Shared Board/List를 사용한다.
- 식품 라벨 OCR은 `extractTextFromImage({ normalize_label_ocr:true })` 또는 `normalizeLabelOcr()` 중 하나만 쓴다.
- OCR/문서/SMS처럼 비용이나 권한이 있는 기능은 실패 UI와 재시도 안내를 둔다.
- 보따리앱 ZIP은 30MB 원본 제한을 기준으로 만든다.
- 설치형 보따리앱이라면 PWA 모드를 켜고 실제 Web App Manifest인 `manifest.json`, `service-worker.js`, 192x192/512x512 앱 고유 아이콘을 포함한다.
- PWA 원본 아이콘에는 보따리 로고를 직접 넣지 않는다. 플랫폼이 설치 아이콘 우하단에 작은 보따리 배지를 자동 합성한다.
- 누락 시 보따리가 기본 PWA 파일을 자동 생성하지만, 앱 고유 아이콘/색/캐시 전략이 있으면 ZIP 파일의 설정을 우선한다.
- PWA 서비스워커는 정적 자원 중심으로 캐시하고 보따리 서버 API 응답은 영구 캐시하지 않는다.
- 앱 내부 설치 안내 버튼은 `BotariSDK.openInstallGuide()`를 호출한다.
- 사용자화가 필요하면 ZIP에 `settings.json`을 넣고 앱에서는 `BotariSDK.getSettings()`만 호출한다. 앱 내부에 별도 설정 저장 UI를 중복 구현하지 않는다.
- 푸시 알림 권한은 사용자가 알림을 받을 상황에서 플랫폼 모달로 요청한다. 앱 시작 직후 직접 `Notification.requestPermission()`을 호출하지 않는다.

## 내 보따리 외부 연동은 앱 SDK가 아님

`/wp-json/botari/v1/external/*`은 사용자가 내 보따리의 공개 상품 정보를 자기 웹사이트·메뉴판·POS 등에서 쓰기 위한 서버 간 API입니다.

- `btex_...` 외부 연동 키를 보따리앱 ZIP, HTML, 브라우저 JavaScript에 넣지 마세요.
- 보따리앱 내부 기능은 이 API 대신 `BotariSDK`를 사용합니다.
- 외부 연동 API는 공개 메타데이터만 제공하고 원본 파일을 전달하지 않습니다.
- 상세 규격과 사용처 신고 방식은 `API-GUIDE.md`의 `내 보따리 외부 연동 API`를 확인하세요.

## 현재 사용자 표시 API

현재 사용 가능한 SDK입니다. 방명록, 롤링페이퍼, 예약, 투표, 내 랭킹 강조처럼 앱 화면에 현재 사용자를 표시할 때 사용합니다.

```js
const me = await BotariSDK.getCurrentUser();

if (me.logged_in) {
  console.log(me.user.display_name, me.user.avatar_url, me.user.profile_url);
}
```

로그인 응답:

```js
{
  logged_in: true,
  user: {
    id: 'stable_public_user_id_or_hash',
    display_name: 'josh',
    avatar_url: 'https://...',
    profile_url: 'https://josh.botari.net'
  }
}
```

비로그인 공개 실행 응답:

```js
{
  success: true,
  logged_in: false,
  user: null
}
```

정책:

- `user.id`는 내부 DB ID가 아닌 안정적인 공개 해시입니다.
- email, phone, raw `user_login`, role/capabilities, 내부 DB user ID는 반환하지 않습니다.
- 앱에서는 내부 요청 경로나 사용자 쿠키를 직접 읽지 말고 이 SDK만 사용합니다.