<!--
  BBBase 연동 가이드 (AI 에이전트용 · 벤더 중립)
  GUIDE_VERSION: 1.10.0
  최신본: https://api.bbbase.io/llms.txt   (서버가 항상 최신으로 제공)

  이 파일은 단일 소스다. CLI(`bbbase init`)가 고객 레포에 떨구고,
  서버 `/llms.txt` 가 그대로 서빙한다. 내용 수정 시 GUIDE_VERSION 을 올린다.
-->

# BBBase 연동 가이드 (AI 에이전트용)

이 문서는 어떤 AI 코딩 어시스턴트(Claude, Cursor, Codex, Cline, Copilot 등)든
**BBBase BaaS 를 정확히 연동**하도록 돕는 벤더 중립 지침이다. 기본 연동은 **REST 호출**이다 —
`fetch`/`UnityWebRequest`/`HttpClient` 등으로 직접 호출한다(이 문서의 규약이 그대로 적용).
**공식 SDK 도 있다** — **Unity**(`.unitypackage`, `BBBaseSdk` 네임스페이스, `Assets/BBBase/`)와
**Godot**(GDScript, `addons/bbbase/`, autoload `BBBase`). 프로젝트에 이 중 하나가 보이면 REST
코드를 직접 짜지 말고 SDK API 를 사용하라. SDK 사용법은 Unity 는 `{BASE_URL}/quickstart/unity`,
Godot 은 `{BASE_URL}/quickstart/godot` 가 권위다(인증/envelope/compareMode 규약은 이 문서와
동일하며, SDK 가 헤더·파싱·세션을 대신 처리한다).

> 🌐 **게임이 브라우저에서 도는 빌드**(순수 JS/TS 웹게임, Godot Web/WASM, Unity WebGL,
> 앱인토스 미니앱)라면 코드를 짜기 전에 **§3.8 을 먼저 읽어라** — origin(CORS) 등록이
> 선행 필수이며, 안 하면 로그인 요청부터 브라우저가 차단한다.

> **이 가이드의 권위 분담 (중요):**
> - **변하지 않는 규약**(인증 종류, 응답 envelope, compareMode, 에러코드 의미)은 **이 문서**가 권위다.
> - **정확한 경로·필드·타입**은 라이브 OpenAPI `{BASE_URL}/docs-json` 이 권위다 — 서버가
>   코드에서 자동 생성하므로 **항상 최신**. 이 문서에 없는 엔드포인트/필드를 다루거나 이
>   문서가 오래돼 보이면 `/docs-json` 을 믿어라.
> - 이 문서 자체의 최신본은 **`{BASE_URL}/llms.txt`** 에 있다.

## 0. 자격증명 먼저 확보

호출에는 **BASE_URL · PROJECT_ID · API_KEY** 가 필요하다. 셋의 성격이 다르다:

- **BASE_URL** — 먼저 `BBBase_Keys.md` 나 게임 설정에 BASE_URL 이 있으면 **그 값을 쓴다**(예:
  개발 단계엔 dev 서버 주소를 적어 둠). 없으면 **기본값 `https://api.bbbase.io`(prod API)** 를
  쓴다 — 이 경우 물어보지 말고 기본값을 적용하라. 대부분의 출시 게임은 prod 기본값을 그대로
  쓰고, BBBase 를 직접 운영하며 dev 서버에서 먼저 개발하는 경우에만 `BBBase_Keys.md` 에 dev
  주소를 넣는다. (대시보드는 `https://bbbase.io`, API 는 `https://api.bbbase.io` 로 다르다.)
- **PROJECT_ID · API_KEY** — 프로젝트마다 다른 값이다. 프로젝트 루트의 `BBBase_Keys.md` 나 게임
  환경설정에서 읽고, 없으면 **개발자에게 직접 물어본다**(추측·공란 금지).
- BBBase 프로젝트 자체가 아직 없으면(계정/프로젝트/키 미발급) 개발자가 대시보드
  (`https://bbbase.io`) 또는 `bbbase` CLI 로 먼저 발급해야 한다.

> ⚠️ `API_KEY` 는 게임 클라이언트에 임베드되는 **공개 취급** 키지만, 그래도 소스
> 형상관리에 커밋하지 않는다(키 로테이션 용이). 발급 키는 `BBBase_Keys.md` 에 적고
> `.gitignore`(SVN `svn:ignore`, Perforce `.p4ignore`)에 추가한다.

## 1. 인증 — 세 종류를 구분하라 (가장 흔한 실수)

호출하는 API 에 따라 헤더가 **다르다**. 섞으면 401.

| 인증 | 헤더 | 적용 대상 | 누가 호출 |
|---|---|---|---|
| **API 키** | `X-API-Key: {API_KEY}` | 게임 데이터(레코드, 엔티티, 랭킹 조회) | 게임 클라이언트 |
| **게임유저 토큰** | `Authorization: Bearer {accessToken}` | 레코드 호출 시 API 키와 **함께** — 본인 신원 증명 | 게임 클라이언트(플레이어) |
| **운영자 JWT** | `Authorization: Bearer {accessToken}` | 관리(스키마, 리더보드/유니크 정의, 리셋잡, 감사로그, 프로젝트·키 관리) | 운영자/콘솔/CLI |

경계선:
- **게임 런타임에서 매번 호출**하는 건 거의 다 **API 키** (점수 저장, 기록 조회, 랭킹).
- **셋업/정의/운영**(한 번 또는 가끔)은 **운영자 JWT** (컬럼 정의, 리더보드 등록, 리셋 설정).
- 리더보드는 둘로 갈림 — **정의 등록은 운영자 JWT, 랭킹 조회는 API 키**.

> **게임유저 토큰 ≠ 운영자 JWT** (둘 다 `Bearer` 지만 전혀 다른 토큰). 게임유저 토큰은
> 플레이어가 **게스트 로그인**(`POST /projects/:pid/auth/guest`) 또는 **소셜 로그인**
> (`POST /projects/:pid/auth/google` · `.../auth/apps-in-toss`)으로 받는다 — 어느 방식이든
> 발급되는 토큰·userId 체계는 동일하다. 레코드 호출 때 API 키와 함께 붙이면 서버가 "경로의
> userId 가 진짜 본인인지" 확인해 남의 레코드 조작(403)을 막는다. **userId 는 BBBase 가 로그인
> 시 발급** — 직접 만들지 말 것.
>
> 소셜 로그인은 **신원 확인만 외부 IdP 에 위임**한다: 구글은 클라이언트가 받은 `idToken` 을,
> 앱인토스는 `authorizationCode` 를 BBBase 가 검증해 게임유저를 발급한다(클라이언트가 구글
> `idToken` 을 받는 부분은 게임 SDK 의 몫 — Play Games(PGS)가 아니라 **구글 계정 로그인**이어야
> idToken 이 나온다). 프로바이더별 client ID 는 운영자가 미리 등록한다(`auth-provider:set`).

## 2. 응답 형식 — 항상 envelope

```json
{ "success": true,  "data": { ... } }
{ "success": false, "error": { "code": "ERROR_CODE", "message": "설명" } }
```

게임 코드는 **`error.code` 로 분기**한다(`message` 는 사람용, 바뀔 수 있음). 실제 데이터는
`data` 안에 있다(레코드는 `data.data` 가 JSONB).

## 3. 가장 흔한 작업 — 유저 레코드 저장/불러오기

유저 1명당 프로젝트 1개에 레코드 1개. 모든 값은 `data`(JSONB) 한 곳에 들어간다.

```bash
# 불러오기
curl {BASE_URL}/projects/{PROJECT_ID}/entities/user/{userId}/record \
  -H "X-API-Key: {API_KEY}"

# 저장 (upsert — 신규 생성 또는 compareMode 규칙으로 병합)
curl -X PUT {BASE_URL}/projects/{PROJECT_ID}/entities/user/{userId}/record \
  -H "Content-Type: application/json" -H "X-API-Key: {API_KEY}" \
  -d '{ "data": { "best_time": 4.35, "stars": 120 } }'
```

저장은 **단순 덮어쓰기가 아니다.** 각 컬럼은 스키마에 정의된 `compareMode` 로 병합된다:

| compareMode | 동작 | 쓰임새 |
|---|---|---|
| `NONE`(기본) | 항상 덮어씀 | 닉네임, 설정값 |
| `MIN` | 더 작을 때만 갱신 | 레이스 타임(`best_time`) |
| `MAX` | 더 클 때만 갱신 | 최고 점수, 최고 스테이지 |
| `INCREMENT` | 기존값 + 보낸값 (원자적, 동시성 안전) | 누적 카운트 |
| `BITSET` | 비트 OR (한 번 켜지면 유지) | 이탈구간·퍼널 도달 단계 |

> `BITSET` 은 비트마스크를 OR 로 합친다 — 단계 N 도달 시 `1 << N` 의 **10진 문자열**(예: 6단계 = `"64"`)을
> 저장하면 서버가 기존 값과 OR. 컬럼 타입은 `STRING`, 기본값 `"0"` 으로 정의한다(큰 비트 안전). 이탈구간
> 분석에 쓴다 — 자세히는 아래 4번 "분석" 참고.

클라이언트는 "현재 기록이 더 좋은지" 비교할 필요 없이 그냥 PUT 한다 — 서버가 `MIN`/`MAX`
로 막아준다. 동시 저장으로 인한 재화 중복도 서버가 락으로 방지한다. 단, 컬럼은 **미리 스키마에
정의돼 있어야** 한다(없으면 `UNKNOWN_COLUMN`). 게임유저 인증이 켜진 환경에선 이 호출에
`Authorization: Bearer {accessToken}` 도 함께 붙여야 본인 레코드로 통과한다.

## 3.5 Unity SDK 가 설치돼 있다면 — REST 대신 SDK

프로젝트에 `Assets/BBBase/`(또는 코드에 `using BBBaseSdk;` / `BBBase.Init()`)가 보이면
**BBBase Unity SDK 가 설치된 것**이다. 이때는 위의 `UnityWebRequest` 호출을 손수 짜지 말고
SDK API 를 써라(헤더·envelope·404·세션을 SDK 가 대신 처리). 규약은 위와 100% 동일하다.

```csharp
using BBBaseSdk;

BBBase.Init();                                              // 1회 (Resources/BBBaseSettings)
await BBBase.Auth.LoginGuestAsync();                        // 또는 LoginGoogleAsync(idToken)
await BBBase.Records.SaveMineAsync(new { best_time = 4.35 }); // 내 레코드 저장(compareMode 병합)
var me  = await BBBase.Records.LoadMineAsync();             // 없으면 null
var top = await BBBase.Leaderboards.GetTopEntriesAsync("LB_ID", 10);
var lg  = await BBBase.Leagues.GetMyStatusAsync("LEAGUE_ID");   // 리그 현황(Tier/Rank/Score)
var box = await BBBase.Mails.GetMailboxAsync();                 // 내 우편함(미수령)
var cl  = await BBBase.Mails.ClaimAsync(box[0].Id);             // 수령 — 서버가 재화 원자 지급(멱등)
// 실패는 BBBaseException → e.Code(BBBaseErrorCodes)로 분기. userId 는 BBBase.UserId.
```

설치/전체 API 표면은 `{BASE_URL}/quickstart/unity` 가 권위이며, 정확한 메서드 시그니처는
프로젝트의 `Assets/BBBase/Runtime/**` 소스(XML 문서주석)가 권위다. SDK 를 쓰지 않기로 했거나
다른 엔진이면 이 문서의 REST 방식대로 진행한다.

## 3.6 Godot SDK 가 설치돼 있다면 — REST 대신 SDK

프로젝트에 `addons/bbbase/`(또는 코드에 `BBBase.init()` autoload 호출)가 보이면 **BBBase Godot
SDK(GDScript)가 설치된 것**이다. 이때는 `HTTPRequest` 호출을 손수 짜지 말고 SDK API 를 써라.
규약은 위와 100% 동일하지만, **GDScript 엔 예외가 없어 모든 호출이 `BBBaseResult` 를 반환**한다 —
`res.ok` 로 분기하고 성공 시 `res.data`, 실패 시 `res.error_code`(`BBBaseErrorCodes` 상수)를 본다.

```gdscript
BBBase.init()                                          # 1회 (res://bbbase_settings.tres)
var login := await BBBase.auth.login_guest()           # 또는 login_google(id_token)
if not login.ok: push_error(login.error_code)
await BBBase.records.save_mine({ "best_time": 4.35 })  # 내 레코드 저장(compareMode 병합)
var me := await BBBase.records.load_mine()             # me.data (없으면 null)
var top := await BBBase.leaderboards.get_top_entries("LB_ID", 10)
var lg := await BBBase.leagues.get_my_status_mine("LEAGUE_ID")  # 리그 현황(res.data.tier/rank/score)
var box := await BBBase.mails.get_mailbox()                    # box.data = 우편 배열(미수령)
var cl := await BBBase.mails.claim(box.data[0].id)             # 수령 — 서버가 재화 원자 지급(멱등)
# userId 는 BBBase.user_id(). load_*/delete_* 는 없을 때 ok=true, data=null.
```

설치/전체 API 표면은 `{BASE_URL}/quickstart/godot` 가 권위이며, 정확한 시그니처는 프로젝트의
`addons/bbbase/runtime/**` 소스가 권위다. Godot 4.1+ / GDScript 기준이다.

## 3.7 계정 링킹 + 클라우드 세이브

**클라우드 세이브는 이미 되고 있다.** 유저 데이터는 처음부터 BBBase 서버(레코드)에
`userId` 로 저장되니, 같은 계정으로 로그인하면 어느 기기에서든 그대로 불러온다. 빠진 건
**"게스트 진행도를 잃지 않고 소셜 계정에 묶는" 링킹**뿐이다.

핵심 규칙: **링킹해도 `userId` 는 바뀌지 않는다.** 한 계정에 여러 로그인 수단(게스트·구글·
앱인토스)을 붙이는 것이라, 세이브(레코드)는 그대로 따라온다.

```
POST   /projects/:pid/auth/link            (API 키 + 게임유저 토큰)  body {provider, idToken|authorizationCode|deviceId, referrer?}
DELETE /projects/:pid/auth/link/:provider  (API 키 + 게임유저 토큰)  provider = GUEST|GOOGLE|APPS_IN_TOSS
GET    /projects/:pid/auth/me              (API 키 + 게임유저 토큰)  → { userId, isGuest, providers:[...] }
```

전형적 흐름:
- **게스트로 시작 → 나중에 구글 연동**: 게스트 로그인 상태에서 `idToken` 을 받아
  `POST /auth/link {provider:"GOOGLE", idToken}`. 진행도(레코드) 그대로, 이제 구글로도 로그인 가능.
- **기기 변경 복구**: 새 기기에서 그냥 **구글 로그인**(`/auth/google`)하면 끝 — 그 구글이
  가리키는 기존 계정으로 로그인되어 세이브가 복구된다(링킹이 아니라 그냥 로그인).
- **링크 해제**는 `DELETE /auth/link/:provider`. 단 **마지막 남은 수단은 해제 불가**
  (`CANNOT_UNLINK_LAST`) — 로그인 수단이 0개가 되는 걸 막는다.

> ⚠️ **충돌(`409 IDENTITY_ALREADY_LINKED`)**: 링크하려는 소셜 계정이 **이미 다른 계정**에
> 묶여 있을 때 난다(예: 두 기기에서 각각 게스트로 진행한 뒤 한쪽에 이미 연동한 구글을 다른
> 쪽에 또 링크). 서버는 자동으로 머지하지 않고 거부하며, `error.details.conflictUserId` 로
> 상대 계정을 알려준다. 게임은 "기존 계정으로 전환할까요?(현재 진행도는 사라집니다)" 같은
> 선택 UI 를 띄우고, 전환을 택하면 그 소셜로 **로그인**(`/auth/google`)해 기존 계정으로 넘어간다.

SDK 설치 시: Unity `BBBase.Auth.LinkGoogleAsync/UnlinkAsync/GetMeAsync`, Godot
`BBBase.auth.link_google/unlink/get_me` — 위 규약 그대로다.

## 3.8 웹(브라우저) 빌드 — **origin 등록이 선행 필수**

게임이 **브라우저에서 도는 빌드**면(순수 JS/TS 웹게임, Godot Web/WASM, Unity WebGL,
앱인토스 미니앱 등) 코드를 짜기 전에 **CORS origin 등록부터** 해야 한다. 네이티브 빌드
(Unity/Godot 데스크톱·모바일)는 브라우저가 아니므로 이 절과 무관하다.

> 🚨 **등록 전에는 로그인 요청 자체가 브라우저에서 차단된다.** 그리고 증상이
> `Access-Control-Allow-Origin` / `blocked by CORS policy` 브라우저 에러로 나타나기 때문에
> **자기 `fetch` 코드 버그로 오진하기 쉽다.** BBBase 호출에서 CORS 에러가 보이면 코드를
> 고치지 말고 **먼저 origin 이 등록됐는지 확인하라.** `mode:'no-cors'` 같은 우회는 응답을
> 읽을 수 없게 만들 뿐 해결책이 아니다.

등록은 운영자가 한다(서버 재배포 불필요, 즉시 반영):

```bash
# CLI
bbbase origin:add {PROJECT_ID} --origin https://my-game.example.com
bbbase origin:list {PROJECT_ID}
```

대시보드에서는 **프로젝트 → 소셜 로그인 설정 → 허용 Origin (CORS)** 카드에서 같은 일을 한다.
운영자 권한이 없다면 이 값은 에이전트가 추측하지 말고 **개발자에게 등록을 요청**한다.

- **origin 은 스킴+호스트+포트까지 정확히** 일치해야 한다(`https://a.com` ≠ `http://a.com` ≠
  `https://a.com:8443`). 경로(`/game`)는 origin 이 아니므로 넣지 않는다.
- **로컬 개발 주소도 따로 등록**해야 한다(예 `http://localhost:5173`). 배포 origin 만 등록하면
  개발 중에는 계속 막힌다.

## 3.9 웹에서의 게스트 로그인 — `deviceId` 를 무엇으로 채울까

게스트 로그인은 `POST /projects/:pid/auth/guest` 에 **`deviceId`(문자열, 최대 256자)** 하나를
보낸다. 서버는 이 값을 게스트 신원의 식별키로 삼아 find-or-create 한다 — **같은 값으로 다시
로그인하면 같은 `userId`, 같은 세이브**다. 즉 `deviceId` 는 "이 값을 잃으면 계정을 잃는" 값이다.

```js
const res = await fetch(`${BASE_URL}/projects/${PROJECT_ID}/auth/guest`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
  body: JSON.stringify({ deviceId }),
});
const { success, data } = await res.json();   // data = { accessToken, refreshToken, userId }
```

무엇을 `deviceId` 로 쓸지는 플랫폼이 정한다 — **더 안정적인 값이 있으면 그걸 쓴다**:

| 환경 | 권장 `deviceId` |
|---|---|
| 앱인토스 미니앱 | `User.getAnonymousKey()` 가 주는 **앱 스코프 익명키(hash)** — 로그인 프롬프트 없이 안정적 |
| 일반 웹 | `localStorage` 에 저장한 `crypto.randomUUID()` (없으면 생성해 보관) |
| 네이티브 | 기기 ID / SDK 가 관리하는 값 |

> ⚠️ **웹의 `localStorage` 는 잘 날아간다**(캐시 삭제·시크릿 모드·다른 브라우저/기기).
> 게스트 계정은 그만큼 쉽게 유실되니, 진행도가 중요한 게임이라면 초반에 **소셜 계정 링킹**
> (`POST /auth/link`, 3.7 참고)을 유도하라. 링킹해도 `userId` 는 바뀌지 않아 세이브가 그대로
> 따라온다. 플랫폼이 주는 안정적 익명키가 있다면 그것부터 쓰는 게 먼저다.

## 4. 그 밖의 기능 — 정확한 모양은 `/docs-json`

아래 기능들은 모두 BBBase 에 있다. **정확한 경로·필드·바디는 `{BASE_URL}/docs-json`**(라이브
OpenAPI)에서 확인하라. 의미·동작 규칙만 여기 적는다:

- **범용 엔티티 레코드** — `entityType` 자유 문자열(group/guild/season 등). 유저 레코드와
  같은 compareMode 병합. 경로는 `/projects/:pid/entities/:entityType/:entityId/record`.
- **등록형 리더보드** — 운영자가 먼저 정의 등록(JWT), 게임은 top-N/내 순위 조회(API 키).
  점수는 레코드 저장 시 전용 테이블에 자동 동기화. 필터는 두 종류: **고정 조건**은 `segment`
  (예: 여성 전용), **값별 그룹 랭킹**(예: 길드별 길드원 기여도)은 정의에 `groupByCol`(컬럼명,
  예 `guild_id`)을 주고 조회 시 `?groupKey={값}` 으로 그 그룹 내 순위를 읽는다 — 길드 수와
  무관하게 정의 1개로 커버. `groupKey` 생략 시 전체 합산 랭킹.
- **리그(League)** — 브론즈/실버/골드 같은 **티어 사다리 + 주기적 승격/강등**. 운영자가 정의
  등록(JWT) 시 랭킹용 리더보드가 자동 생성된다. 게임은 점수 컬럼(기본 `league_points`)만 평소처럼
  저장하면 되고, 주기(일/주/월)마다 서버가 그룹 내 순위로 상위 승격(`tier+1`)/하위 강등(`tier-1`)
  후 점수를 리셋한다. 두 모델: **티어 풀**(한 티어=하나의 랭킹) 또는 **코호트**(한 티어를 N명씩
  방으로 쪼개 방 안에서 승강 — Duolingo 식). 게임 클라는 내 현황(`.../leagues/:id/me/:entityId`)과
  내 그룹 랭킹(`.../leagues/:id/ranks/:entityId`)을 API 키로 조회. 티어/점수 컬럼은 NUMBER 스키마.
- **분석(리텐션·이탈율·이탈구간)** — 대시보드 지표. 이벤트 로그 없이 비트마스크로 집계.
  - **리텐션·이탈율은 자동** — 게임 클라가 추가로 보낼 게 없다. 로그인/레코드 저장만 붙어 있으면
    서버가 가입일 기준 "N일째 접속"을 비트로 쌓고 매일 코호트별 D1/D7/D30 을 굽는다. 이탈율은
    마지막 접속 시각으로 파생. 운영자는 `GET /projects/:pid/analytics/retention|churn` 으로 조회.
  - **이탈구간(퍼널)만 게임이 신호** — "어느 단계에서 떠났나"는 게임만 안다. `BITSET` 컬럼(예
    `funnel`, 타입 STRING)을 운영자가 정의해두고, 게임이 단계 N 도달 시 `1 << N` 의 10진값(예
    6단계 `"64"`)을 평소 레코드 저장처럼 PUT 하면 서버가 OR 누적. 운영자는
    `GET /projects/:pid/analytics/funnel?column=funnel` 로 단계별 도달자(이탈구간)를 본다.
- **우편함(Mailbox)** — 운영자가 개인/전체발송 메일을 보내고(보상 첨부 가능), 게임 클라가
  수령한다. **보상 지급은 서버가 원자적으로** 한다: 게임은 수령 버튼에서 `POST
  /projects/:pid/mailbox/:mailId/claim`(API 키+게임유저 토큰) 한 번만 호출하면 서버가 수령표시와
  재화 누적을 한 트랜잭션에서 처리 — 재수령해도 재화는 안 늘어난다(멱등). 조회는 `GET
  /projects/:pid/mailbox`(미수령만; `?includeClaimed=true` 로 전체), 전체 수령은 `POST
  .../mailbox/claim-all`. ⚠️ **메일 보상으로 줄 재화 컬럼은 반드시 `NUMBER` + `compareMode=INCREMENT`
  스키마여야 한다**(운영자가 그렇게 정의). 그래야 서버가 안전하게 더해줄 수 있고, 그 컬럼은 게임도
  평소 절대값이 아니라 증감분(+획득/−소비)으로 저장해야 한다. 보상 컬럼은 리더보드 집계 컬럼과
  분리하는 걸 권장(안 그러면 우편 보상이 랭킹에 반영됨). 발송은 운영자(JWT) 작업.
- **로그 수집(Logs)** — 게임 클라가 **API 키만으로**(게임유저 토큰 불필요) 임의 이벤트 로그를
  쌓는 채널. `POST /projects/:pid/logs`, 바디 `{ level?, category?, message?, platform?, data? }`
  (전부 선택). **로그인이 실패해서 유저 토큰이 없는 상황**(소셜 토큰 검증 실패 등)의 로그를
  남기는 게 주 용도 — 일반 레코드 API 는 게임유저 토큰이 필요해서 이 케이스를 못 잡는다.
  fire-and-forget 로 보내라(전송 실패가 게임 흐름을 막지 않게). ⚠️ API 키는 공개 식별자라 이
  로그는 **신뢰할 수 없는 제보**다 — 게임 상태/과금에 반영하지 말고 디버깅·통계용으로만.
  조회는 운영자(JWT) `GET /projects/:pid/logs?level=&category=&platform=`(대시보드 "로그" 화면 /
  CLI `log:list`). 기본 30일 보관 후 자동 정리(`LOG_RETENTION_DAYS`).
- **공용 Config(Remote Config)** — 프로젝트 전역 공용 설정값. 게임 클라가 **API 키만으로**(게임유저
  토큰 불필요, **로그인 전에도**) 읽을 수 있어 **필수 업데이트(최소 요구 버전)**·원격 기능 플래그·
  서버 튜닝값 등에 쓴다. 읽기 `GET /projects/:pid/configs/:key`(API 키) → `{ key, value, updatedAt }`,
  없으면 `CONFIG_NOT_FOUND`(404)→ 게임은 "설정 없음=기본 동작"으로 처리. 값은 **운영자(JWT)만**
  바꾼다: `PUT /projects/:pid/configs/:key` 바디 `{ value: <any JSON> }`, 목록 `GET
  /projects/:pid/configs`, 삭제 `DELETE .../configs/:key`(대시보드 "공용 Config" 화면 / CLI
  `config:set|list|get|remove`). 공개 읽기는 Redis 5분 캐시라 값 변경이 최대 5분 뒤 반영된다.
  값은 작은 설정(≤32KB)용 — 이미지·에셋 같은 **파일은 담지 마라**(스토리지의 몫). 예(필수 업데이트):
  운영자가 `force_update = { "minVersion":"1.2.0", "storeUrl":"...", "message":"..." }` 저장 →
  게임이 시작 시 조회해 현재 버전과 비교, 낮으면 스토어로 유도하고 진입 차단.
- **유니크 제약** — 닉네임/길드명 중복 금지. 운영자가 사전 등록(컬럼 지정). 이후 중복 값
  저장 시 `409 DUPLICATE_VALUE` 로 차단(DB 레벨 강제).
- **주기 리셋** — 일/주/월 단위로 특정 컬럼 자동 리셋(운영자가 리셋잡 등록).
- **감사로그** — 레코드 변경 이력 조회(운영자 JWT).
- **게임유저 로그인** — 게스트 + 소셜 로그인(구글 `idToken`, 앱인토스 `authorizationCode`).
  토큰 발급·refresh·로그아웃. 프로바이더 client ID 는 운영자가 사전 등록.
- **계정 링킹 + 클라우드 세이브** — 게스트 진행도를 잃지 않고 소셜 계정에 연동(`userId` 불변).
  `link`/`unlink`/`me` (§3.7). 충돌은 `409 IDENTITY_ALREADY_LINKED`(자동 머지 안 함).

> ⚠️ **운영자 셋업이 필요한 작업은 먼저 개발자에게 물어라.** 위에서 "운영자가 등록/정의"라고
> 적힌 것(리더보드·유니크 제약 등록, 스키마 컬럼 정의, 리셋잡, 소셜 프로바이더 설정)은
> 운영자(JWT) 권한이라 게임 코드만으로 안 된다. "랭킹 붙여줘" 같은 요청이 오면 게임 코드(조회)만
> 짜고 끝내지 말고 **누가 셋업할지** 물어라 — ① 에이전트가 직접(CLI/REST) 추가(→ **운영자
> 로그인 정보 필요**, 게임 API_KEY 와 다른 자격증명), 또는 ② 개발자가 대시보드(`https://bbbase.io`)
> 에서 직접. ②면 무엇을 만들지(예: `best_time` NUMBER MIN 컬럼 + 그 컬럼 ASC 리더보드) 정확히
> 안내한다. 운영자 자격증명은 추측·하드코딩 금지.

## 5. 자주 만나는 에러코드

| code | 의미 | 대처 |
|---|---|---|
| `UNKNOWN_COLUMN` | 스키마에 없는 컬럼 저장 | 먼저 스키마에 컬럼 정의 |
| `UNKNOWN_ENTITY_TYPE` | user 외 entityType 인데 그 scope 의 스키마가 하나도 없음 | 먼저 그 scope 로 스키마 정의(오타 확인) |
| `DUPLICATE_VALUE` (409) | 유니크 컬럼에 이미 쓰인 값 | 다른 값 요청(닉네임 중복 안내) |
| `RECORD_NOT_FOUND` / `ENTITY_RECORD_NOT_FOUND` | 레코드 없음 | 신규 유저로 처리 |
| `RATE_LIMIT_EXCEEDED` / `TOO_MANY_REQUESTS` | 호출 한도 초과 | 백오프 후 재시도 |
| `LEADERBOARD_SCORE_NOT_FOUND` | 랭킹에 점수 없음 | "기록 없음" 표시 |
| 401 / `UNAUTHORIZED` | 인증 헤더 잘못됨 | API키 vs 게임유저 토큰 vs 운영자 JWT 확인(§1) |
| 403 / `FORBIDDEN` | 남의 userId 로 레코드 접근 | 경로 userId 를 로그인 응답의 userId 로 교정 |
| `USER_BANNED` (403) | 운영자가 제재한 계정 | 재시도·재로그인 금지. `details.expiresAt`(null=영구)·`details.reason` 으로 정지 안내 화면 표시 |
| `IDENTITY_ALREADY_LINKED` (409) | 링크하려는 소셜이 이미 다른 계정에 묶임 | `details.conflictUserId` 로 전환 여부를 유저에게 묻기(§3.7) |
| `CANNOT_UNLINK_LAST` (400) | 마지막 로그인 수단 해제 시도 | 다른 수단을 먼저 연동 후 해제 |
| `INVALID_REWARD_COLUMN` (400) | 메일 보상 컬럼이 NUMBER+INCREMENT 아님 | 재화 컬럼을 INCREMENT 로 정의(운영자) |
| `MAIL_NOT_FOUND` (404) | 메일 없음/대상 아님 | 우편함 목록을 다시 조회 |
| `MAIL_EXPIRED` (410) | 만료된 메일 수령 시도 | "만료됨" 표시, 목록 갱신 |
| `CONFIG_NOT_FOUND` (404) | 그 Config 키 없음 | "설정 없음=기본 동작"으로 처리(게임 죽이지 말 것) |

> 🌐 **`blocked by CORS policy` / `Access-Control-Allow-Origin` 브라우저 에러는 위 표에 없다** —
> BBBase 응답 자체가 오지 않은 것이라 `error.code` 가 존재하지 않는다. 코드 버그가 아니라
> **origin 미등록**이 원인이니 §3.8 대로 등록부터 확인하라.

Rate limit: 게임 데이터 API 는 **API 키당 분당 600회**, 인증 API 는 IP당 분당 10회.
요청 본문 최대 256KB.

## 6. 작업 원칙

- 엔드포인트 경로·필드명을 **추측하지 말 것.** 우선순위: ① 이 문서 →
  ② `GET {BASE_URL}/docs-json`(라이브 OpenAPI) → ③ 그래도 모르면 개발자에게 질문.
  `/docs-json` 이 404 면 Swagger 비활성 상태이니 ①·③ 으로만 진행.
- BBBase 호출은 응답을 `success` 로 분기하고 `error.code` 별 처리를 넣는다 —
  네트워크/서버 오류로 게임이 죽지 않게.
- API_KEY/토큰을 코드에 하드코딩하지 말고 설정/환경값으로 분리한다.
- **(Windows) curl 로 한글 등 비ASCII 를 보낼 때 인라인 `-d '{"name":"한글"}'` 금지** —
  셸 코드페이지 때문에 인코딩이 깨진다. UTF-8 파일로 저장 후 `curl --data @body.json
  -H "Content-Type: application/json; charset=utf-8"` 로 보내거나 대시보드 UI 로 입력.
  (컬럼명은 항상 `^[a-z][a-z0-9_]*$` 라 영어다.)
