랭킹 API 사용법
내 게임에 이 오락실의 회원 로그인 + 점수 랭킹을 붙이고 싶다면, 아래 프롬프트를 Claude Code / Codex에 그대로 붙여넣기만 하면 됩니다. 나머지는 AI가 지침을 읽고 알아서 처리합니다.
이 게임에 개발놀이터 오락실 랭킹 연동을 셋팅해줘. 지침: https://game.dev-play.kr/api-guide.php?format=md 지침을 읽고 ①게임 등록(소유권 확정) ②SDK 삽입 ③게임오버 시 점수 제출 연결 ④연동 검증(등록 확인·테스트 제출) ⑤이미 달성한 업적 자동 인증까지 완료하고 결과를 알려줘.
로그인하면 프롬프트에 내 API 토큰이 자동 포함되어, 게임 소유권 확정까지 AI가 한 번에 처리합니다.
내 API 토큰 직접 보기 (서버측 등록·CI용)
보통은 위 프롬프트만으로 충분합니다(토큰이 지침 주소에 포함됨). 토큰 값을 직접 다뤄야
할 때만 여기서 확인하세요. 공개 저장소 코드에 하드코딩 금지 —
환경변수(DEVPLAY_RANK_TOKEN)로만.
AI 에이전트용 셋업 지침
아래는 AI 코딩 에이전트가 그대로 따라 실행하는 완전한 지침입니다(사람은 위 "가장 쉬운 방법"만 보면 됩니다).
에이전트가 기계로 읽기 좋은 원문만 필요하면
https://game.dev-play.kr/api-guide.php?format=md
를 요청하세요(text/markdown 원문 반환).
# 개발놀이터 오락실 — 랭킹 연동 AI 에이전트 셋업 지침
당신은 스터디원이 만든 웹게임 저장소를 수정하는 AI 코딩 에이전트입니다.
아래 단계를 순서대로, **멱등하게**(이미 되어 있으면 건너뛰기) 수행하세요.
이 지침대로 코드/설정을 저장소에 넣어 두면, 당신의 컨텍스트가 초기화된 뒤에도
연동은 계속 동작합니다.
## 0. 개념 요약
- 이 사이트(https://game.dev-play.kr)는 회원 로그인과 게임별 점수 랭킹을 제공합니다.
- 연동은 두 가지뿐입니다: (1) 게임 점수 제출, (2) 게임을 오락실에 자동 등록.
- 게임은 `game_code`(슬러그)로 식별합니다. 이 저장소용 코드를 하나 정하세요
(소문자/숫자/하이픈, 최대 50자, 예: `my-tetris`). 정규식: `^[a-z0-9][a-z0-9_-]{0,49}$`.
- 아래에서 `YOUR-GAME-CODE`, `게임 표시 이름`을 실제 값으로 바꾸세요.
- **pair 파라미터**: 사용자가 준 이 페이지 URL에 `?pair=<값>`이 붙어 있다면,
그 값이 곧 **사용자의 API 토큰**입니다(로그인 상태에서 복사한 주소에 자동 포함됨).
**pair 값이 있으면 셋업 중 4번의 등록 curl을 지금 즉시 1회 실행하세요(필수).**
게임 소유권이 사용자 계정으로 확정됩니다. 6번(검증)에도 같은 값을 씁니다. 값 검증:
`curl -H "Authorization: Bearer <값>" https://game.dev-play.kr/api/v1/me.php` 가 200이면 유효.
이 값을 **공개 저장소 코드에 하드코딩하지 말고** 환경변수(`DEVPLAY_RANK_TOKEN`)로만 저장하거나,
1회 등록만 할 경우 저장 없이 즉시 사용 후 버려도 됩니다. 클라이언트 코드에는 절대 넣지 마세요.
## 1. SDK 스크립트 추가 (점수 제출 + 자동 등록) — 필수
게임의 메인 HTML(또는 진입 스크립트)에 아래를 추가합니다. 이미 있으면 다시 넣지 마세요.
```html
<script src="https://game.dev-play.kr/sdk/rank.js"></script>
<script>
DevplayRank.init({
gameCode: 'YOUR-GAME-CODE', // 이 게임 고유 슬러그
gameName: '게임 표시 이름', // 오락실 목록에 보일 이름
selfRegister: true // 최초 로그인 시 이 게임을 오락실에 자동 등록
// game_url은 배포된 주소에서 자동 감지됩니다(직접 줄 수도 있음: gameUrl: '...').
});
</script>
```
**버전 쿼리(`?v=...`)를 붙이지 마세요.** 서버가 `Cache-Control: no-cache`로 내려 매
로드마다 재검증하므로(변경 없으면 304, 수백 바이트) SDK 수정이 즉시 반영됩니다.
버전 쿼리를 달면 오히려 낡은 값이 코드에 남아 관리 대상이 됩니다.
## 1.1 공통 버튼 3종 — 필요 없는 것만 끄세요
SDK 한 줄로 아래 세 버튼이 **모두 자동으로** 생깁니다. 게임에 이미 같은 기능이
있으면 해당 버튼만 끄고, 게임의 기존 버튼에 SDK 동작을 연결할 수도 있습니다.
| 버튼 | 위치 | init 옵션 | 끄면 대신 쓸 API |
| --- | --- | --- | --- |
| 🏆 랭킹 | 우하단 | `rankButton` | `DevplayRank.open()` |
| 🔊 사운드 | 우상단 | `volumeButton` | `DevplayVolume.toggle()` |
| ✕ 나가기 | 좌상단 | `exitButton` | `DevplayExit.open()` |
각 옵션은 **`true`(기본, 표시) / `false`(숨김) / `'CSS 셀렉터'`(내 버튼에 연결)** 셋 중 하나입니다.
```js
DevplayRank.init({
gameCode: 'YOUR-GAME-CODE',
volumeButton: false, // 인게임에 사운드 토글이 이미 있음 → 공통 버튼 숨김
rankButton: '#myRankBtn' // 내가 디자인한 버튼에 랭킹 패널 열기를 연결
});
```
- 셀렉터를 주면 SDK가 **기본 버튼을 자동으로 숨기고** 그 요소의 클릭에 동작을 붙입니다.
이벤트 위임이라 **버튼을 나중에 만들거나 다시 그려도** 계속 동작합니다.
- 사운드 버튼에 셀렉터를 주면 클릭=음소거 토글, 길게 누르기=볼륨 fader까지 연결되고,
현재 상태가 그 요소에 `data-dp-muted="1|0"`으로 반영됩니다(CSS로 아이콘을 바꾸세요).
- `rankButton: false`여도 `DevplayRank.open()`은 그대로 동작합니다(패널은 살아 있음).
- **에이전트 판단 기준**: 게임 화면을 확인해 같은 기능의 버튼이 이미 있으면 그 버튼을
셀렉터로 연결하고 공통 버튼은 끄세요. 중복 버튼 2개가 나란히 뜨는 것이 가장 나쁩니다.
- 레거시 표기(`<script ... data-exit="off" data-volume="off">`)도 계속 동작합니다.
## 1.5 사운드 버튼 연동 — 필수
SDK는 우상단에 공통 사운드 버튼(🔊/🔇)을 자동으로 붙입니다. **클릭 = 음소거 토글,
길게 누르면(0.5초) 배경음악/효과음 볼륨 fader**가 열립니다. 이 버튼이 게임의 실제
소리를 제어하도록 반드시 연결하세요:
- **게임이 `<audio>`/`<video>` 요소를 쓰는 경우**: 자동으로 적용됩니다(추가 코드 불필요).
단, 배경음악 요소에 `loop` 속성이 없다면 `data-dp-channel="bgm"`을 붙여 채널을 알려주세요
(기본 판정: `loop` 있으면 bgm, 없으면 sfx). 게임이 그 요소의 volume을 직접 제어한다면
`data-dp-channel="off"`로 자동 볼륨 적용을 빼세요(음소거만 SDK가 관리).
- **자체 오디오 엔진(WebAudio/Howler/Phaser/Tone 등)을 쓰는 경우**: 게임의 bgm/효과음
볼륨 지점을 찾아 아래처럼 연결하세요:
```js
function applyDevplaySound(s) { // s = { muted, bgm, sfx } (각 0..1)
bgmGain.gain.value = s.muted ? 0 : s.bgm; // ← 게임의 배경음악 볼륨 지점
sfxVolumeScale = s.muted ? 0 : s.sfx; // ← 효과음 재생 시 곱하는 볼륨
}
applyDevplaySound({ muted: DevplayVolume.isMuted(),
bgm: DevplayVolume.getVolume('bgm'), sfx: DevplayVolume.getVolume('sfx') });
DevplayVolume.onLevels(applyDevplaySound); // fader/토글 변경 시 실시간 반영
```
- **판단 기준**: 게임 코드에서 bgm과 효과음 볼륨 경로가 명확히 나뉘어 있으면 둘 다
연결하세요. 구분이 애매하거나 수정 리스크가 크면 **무리하게 나누지 말고** muted +
bgm 값 하나를 마스터 볼륨으로 전체에 연결하는 심플 모드로 하고, 결과 보고에 그렇게
했다고 명시하세요. (`DevplayVolume`이 없을 수 있으니 `window.DevplayVolume` 존재
체크 후 연결하면 더 안전합니다.)
볼륨 설정(음소거·bgm·sfx)은 localStorage에 오리진 단위로 저장되어 **오락실의 다른
게임에서도 같은 설정으로 시작**합니다. 게임이 별도로 저장할 필요가 없습니다.
## 1.6 SDK가 이미 처리하는 것 — 게임에서 중복 구현하지 마세요
아래 두 가지는 SDK를 넣는 것만으로 자동 적용됩니다. 같은 목적의 코드가 게임에 이미
있다면 **제거하거나 그대로 두어도 무방**하지만, 새로 만들지는 마세요.
**① 오디오 자동재생 정책 해제(unlock)** — 브라우저는 사용자 제스처 없이 소리를 내지
못하게 막습니다. SDK는 화면 **아무 곳의 첫 클릭/터치/키입력**을 명시적 동의로 보고,
페이지의 모든 `AudioContext`를 `resume()`하고 무음 버퍼를 1회 재생해(iOS Safari 대응)
오디오를 열어 줍니다. `autoplay`가 걸린 `<audio>`/`<video>`도 다시 재생을 시도합니다.
**"소리를 켜려면 화면을 탭하세요" 같은 오버레이를 새로 만들 필요가 없습니다.**
```js
// 필요할 때만 쓰는 보조 API (보통은 아무 코드도 필요 없음)
DevplayAudio.isUnlocked() // 이미 열렸는지
DevplayAudio.onUnlock(function () { startBgm(); }) // 열리는 순간 1회 실행
DevplayAudio.register(myAudioCtx) // SDK보다 먼저 만든 AudioContext를 수동 등록
```
SDK는 `AudioContext` 생성자를 감싸 인스턴스를 추적하므로 보통 자동으로 잡힙니다.
다만 **rank.js보다 먼저** 실행되는 코드가 만든 컨텍스트는 놓칠 수 있으니, 그런 구조라면
`DevplayAudio.register(ctx)`를 한 줄 넣거나 rank.js 태그를 게임 스크립트보다 위에 두세요.
**② 더블탭 줌 차단** — 모바일에서 버튼을 연타하면 화면이 확대되는 기본 동작을 막습니다
(`touch-action: manipulation` — **더블탭 줌만** 차단하고 핀치 줌은 유지해 접근성을 지킵니다).
기본 적용 범위는 버튼/링크/입력/`<canvas>`이며, 페이지 전체로 넓히려면
`init({ doubleTapZoom: 'page' })`, 끄려면 `init({ doubleTapZoom: 'allow' })`입니다.
**`<meta viewport>`에 `user-scalable=no`를 추가하지 마세요** — iOS Safari는 이를 무시하고,
동작하는 브라우저에서는 핀치 줌까지 막혀 접근성 문제가 됩니다.
## 2. 점수 제출 — 필수
게임이 끝나는 지점에서 최종 점수(0 이상의 정수)로 호출하세요. **권장은 `gameOver()`** —
점수 제출과 함께 SDK 내장 "게임 종료" 모달(이번 점수 · 최고 점수 · 내 순위 · 랭킹 보기 ·
다시 하기, 우상단 X로 닫기)까지 자동으로 표시합니다. 게임에 결과 UI를 직접 만들 필요가 없습니다.
```js
// 게임오버 시 (권장) — 점수 제출 + 게임 종료 모달 표시
DevplayRank.gameOver(finalScore, { onRestart: function () { restartGame(); } });
// 결과 UI를 게임이 직접 그리는 경우 — 제출만
DevplayRank.submitScore(finalScore, 'score');
```
- `gameOver` 옵션: `onRestart`(다시 하기 콜백, 없으면 버튼 숨김), `submit: false`(이미 제출한 경우 UI만),
`title`(기본 "게임 종료"). X/오버레이 클릭은 **모달만 닫고** 게임 상태는 건드리지 않습니다.
- 로그인돼 있지 않으면 SDK가 점수를 잠시 보관하고 로그인 팝업을 띄운 뒤 자동 제출합니다.
저장소에 **토큰이 필요 없습니다**(팝업 로그인이 처리).
- "낮을수록 좋은" 게임(클리어 타임 등)이면 `init`에 `scoreOrder: 'asc'`를 주세요.
- 클리어 초를 점수로 쓰면 `scoreFormat: 'time'`(랭킹에 "N분 N초"로 표시)도 함께.
**점수 연결 체크리스트(에이전트는 모두 확인하세요)**:
- 게임 코드에서 **실제 게임오버가 확정되는 지점**을 찾아 연결하세요. 종료 경로가
여러 개면(클리어/실패/포기 등) **모든 경로에서 1회씩** 호출되게 하고,
같은 판에서 중복 제출되지 않게 플래그로 방어하세요.
- 점수는 **0 이상의 정수**만 허용됩니다(소수/음수/문자는 400 거부).
소수가 나올 수 있으면 `Math.round()`로 감싸세요.
- `DevplayRank.init()`이 점수 제출보다 **먼저** 실행되는지 확인하세요(스크립트 로드 순서).
- 외부(사이트 밖) 게임의 첫 제출 흐름: 제출 → 401 → SDK가 "로그인할까요?" 모달 →
로그인 팝업 → 토큰 저장 → 자동 재제출. 팝업 차단 시 실패할 수 있다고 사용자에게 알리세요.
## 3. 자동 등록 동작 방식
`selfRegister: true`면, 사용자가 이 게임에서 처음 로그인하는 순간 SDK가
`POST /api/v1/games/register.php`로 게임을 upsert합니다. **저장소에 비밀키가 없습니다.**
- **주의(소유권)**: selfRegister는 "이 게임에서 **처음 로그인한 방문자**"가 소유자(creator)가
됩니다. 사용자보다 다른 사람이 먼저 방문해 로그인하면 `game_code`를 선점당할 수 있습니다.
- 그래서 **pair 토큰이 주어졌다면 selfRegister에 맡기지 말고, 셋업 시점에 4번의 등록 curl을
1회 실행해 소유권을 사용자 계정으로 즉시 확정하세요(필수).** selfRegister는 이후 갱신용
보조 수단으로 그대로 켜 둡니다.
- pair 토큰이 없을 때만: 게임을 배포한 뒤 사용자 본인이 먼저 로그인해 선점하도록 안내하세요.
## 4. 서버측 등록 — pair 토큰이 있으면 셋업 시 1회 필수, 배포/CI 훅은 선택
API 토큰으로 소유권을 확정하는 등록입니다. pair 토큰이 주어졌다면 셋업 과정에서
아래 curl을 **지금 바로 1회** 실행하세요(3번의 소유권 주의 참고). 배포 훅에 넣는 것은 선택입니다:
1. 토큰 얻기 — 우선순위대로:
a. 사용자가 준 URL에 `?pair=<값>`이 있으면 **그 값이 토큰**입니다(0번 참고). 사람에게 더 물을 것 없음.
b. 없으면 사용자에게 요청: "https://game.dev-play.kr/api-guide.php 에 로그인하면 토큰이 항상
표시됩니다(없으면 자동 발급). '프롬프트 복사' 버튼으로 복사해 다시 붙여넣어 주세요."
2. 셋업 시 1회 등록이면 토큰을 어디에도 저장하지 말고 아래 curl에 바로 쓰고 버리세요.
배포/CI 훅에도 넣을 때만 Vercel 프로젝트 환경변수 `DEVPLAY_RANK_TOKEN`에 저장합니다.
**공개 저장소 코드에 토큰을 하드코딩하지 마세요.**
3. 등록 호출(셋업 시 즉시 1회 — 배포 스크립트에 넣는 것은 선택):
```bash
curl -sS -X POST https://game.dev-play.kr/api/v1/games/register.php \
-H "Authorization: Bearer $DEVPLAY_RANK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"code":"YOUR-GAME-CODE","name":"게임 표시 이름","game_url":"https://your-app.vercel.app"}'
```
멱등입니다 — 매 배포마다 호출해도 안전합니다(첫 호출은 생성, 이후는 갱신).
## 4.5 게임 대표 이미지(썸네일 + og:image) — 이미지 생성이 가능하면 첨부하세요
게임 목록 카드에는 대표 이미지(썸네일)가 있어야 보기 좋습니다. **먼저 지금 세션에
이미지를 생성할 수단이 있는지 스스로 점검하세요.** 런타임마다 경로가 다릅니다:
| 실행 환경 | 확인할 수단 |
| --- | --- |
| Codex CLI | 내장 이미지 생성(`imggen` 계열 도구) |
| Claude Code | 연결된 이미지 생성 MCP(예: Higgsfield `generate_image`), 또는 사용 가능한 이미지 생성 API |
| 공통 | 환경변수에 이미지 생성 API 키(OpenAI/Gemini 등)가 있는지, 저장소에 이미 쓸 만한 대표 이미지가 있는지 |
"내 도구 목록에 없다"고 **속단하지 말고** 위 표를 실제로 한 번 확인한 뒤 판단하세요.
저장소에 이미 게임 스크린샷이나 키비주얼이 있다면 그것을 1:1로 크롭해 써도 좋습니다.
- **가능하면(권장)**: 게임을 대표하는 **정사각형(1:1, 512×512 이상 권장)** 이미지를 1장
만드세요. 게임의 실제 화면·주인공·핵심 오브젝트가 한눈에 들어오고 작게 축소해도
알아볼 수 있는 그림이 좋습니다(텍스트/로고 글자는 작게 넣으면 뭉개지니 피하세요).
그런 뒤,
아래처럼 base64로 인코딩해서 4번의 등록 curl에 `thumbnail_base64` 필드로
함께 보내세요. 이미 등록된 게임이라도 다시 호출하면 upsert이므로 썸네일만
갱신됩니다. 형식은 JPEG/PNG/GIF, 5MB 이하이며, 서버가 정사각으로 리사이즈해
저장합니다.
```bash
curl -sS -X POST https://game.dev-play.kr/api/v1/games/register.php \
-H "Authorization: Bearer $DEVPLAY_RANK_TOKEN" \
-H "Content-Type: application/json" \
-d "$(python3 -c '
import base64, json, sys
with open("thumbnail.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
print(json.dumps({
"code": "YOUR-GAME-CODE",
"name": "게임 표시 이름",
"game_url": "https://your-app.vercel.app",
"thumbnail_base64": b64,
}))
')"
# 응답의 game.thumbnail 필드가 null이 아니면 성공. 실패해도 등록 자체는 유지되고
# thumbnail_error 필드로 사유가 옵니다(썸네일은 선택 사항 — 등록을 막지 않습니다).
```
- **그렇게 이미지를 만들었다면, 같은 이미지를 게임 자체의 `index.html`에도
`og:image`로 추가하세요** — 링크 공유(카카오톡/디스코드 등) 시 미리보기
이미지로 쓰입니다. 절대 URL이어야 합니다:
```html
<meta property="og:title" content="게임 표시 이름" />
<meta property="og:description" content="한 줄 설명" />
<meta property="og:image" content="https://your-app.vercel.app/img/thumbnail.png" />
<meta property="og:url" content="https://your-app.vercel.app/" />
```
- **이미지 생성 수단이 정말 없으면**: 조용히 건너뛰세요. `thumbnail_base64`는
선택 필드이므로 생략해도 등록은 정상 동작합니다 — 사용자에게 억지로 이미지를
만들어달라고 요구하지 마세요. 다만 결과 보고에 "썸네일은 생성 수단이 없어
건너뛰었고, 나중에 `register.php`를 다시 호출하면 추가할 수 있다"고 한 줄 남기세요.
## 5. 보안 메모 (위협 모델)
- 이 랭킹은 **친목 스터디용**입니다. API 토큰은 "점수 제출/게임 등록" 권한만 있고
회원 계정 자체를 탈취하지는 못합니다. 그래도 토큰은 공개 코드에 넣지 말고 환경변수로.
- 권장 조합: **셋업 시 pair 토큰으로 4번 등록 1회(소유권 확정) + 이후는 3번 self-register**.
등록 curl은 저장소에 아무것도 남기지 않으므로 이 조합도 "저장소에 비밀 없음"이 유지됩니다.
- 토큰은 사실상 만료 걱정이 없고(10년), 로그인하면 api-guide.php에서 언제든 다시 확인할 수 있습니다.
(친목 스터디용 위협 모델 — 이 토큰으로는 점수 제출/게임 등록만 가능하고 비용 발생·계정 탈취가 불가.)
## 6. 검증 (에이전트가 직접 실행해 성공을 확인하세요)
등록 확인 — 공개, 토큰 불필요:
```bash
curl -sS "https://game.dev-play.kr/api/v1/rankings.php?game_code=YOUR-GAME-CODE"
# → {"ok":true,"game":{...},"rankings":[...]} 이면 게임 등록 성공
```
토큰 유효성 확인(4번 경로를 쓸 때):
```bash
curl -sS https://game.dev-play.kr/api/v1/me.php -H "Authorization: Bearer $DEVPLAY_RANK_TOKEN"
# → {"ok":true,"member":{"id":..,"nickname":".."}}
```
점수 1건 테스트 제출 — **낮은 점수(1)로 하세요**(실제 랭킹에 사용자 기록으로 남습니다.
best-keeps 정책이라 이후 실플레이 점수로 자연히 덮입니다):
```bash
curl -sS -X POST https://game.dev-play.kr/api/v1/scores.php \
-H "Authorization: Bearer $DEVPLAY_RANK_TOKEN" -H "Content-Type: application/json" \
-d '{"game_code":"YOUR-GAME-CODE","score":1}'
# → {"ok":true,"best_score":..,"improved":..,"rank":..}
```
**클라이언트 최종 검증(필수)** — 위 curl은 서버 경로만 확인합니다. 브라우저 경로는
사용자에게 이렇게 요청해 확인하세요: "게임을 한 판 플레이해서 ① 게임 종료 모달에
'내 순위'가 숫자로 표시되는지, ② 우상단 사운드 버튼 클릭으로 소리가 꺼지고 켜지는지,
③ 버튼을 길게 눌러 나오는 fader로 배경음악/효과음 볼륨이 실제로 변하는지, ④ 화면을
한 번 터치한 뒤 소리가 정상 재생되는지, ⑤ (휴대폰) 버튼을 빠르게 두 번 눌렀을 때
화면이 확대되지 않는지 확인해 주세요.
(외부 게임 첫 플레이라면 로그인 팝업이 한 번 뜨는 것이 정상입니다.)"
'점수 기록에 실패했어요'가 뜨면 아래 트러블슈팅을 따르세요.
버튼 옵션을 조정했다면 브라우저 콘솔에서 이것도 확인하세요:
```js
DevplayRank.version // 로드된 SDK 버전 — 값이 나오면 최신 SDK가 실행 중
```
## 6.5 트러블슈팅 — "점수 기록에 실패했어요"가 뜰 때
브라우저 개발자도구 Network 탭에서 `scores.php` 요청의 상태를 확인하고 대조하세요:
| 증상/상태 | 원인 | 해결 |
| --- | --- | --- |
| 401 unauthorized | 비로그인(정상 흐름) | SDK가 로그인 모달을 띄웁니다. 팝업 차단이면 해제 후 재시도 |
| 404 game_not_found | 게임이 미등록 | 4번의 등록 curl 실행(또는 selfRegister 로그인 1회) |
| 400 invalid_score | 점수가 정수가 아님 | `Math.round()` 적용, 음수 방지 |
| 400 invalid_game_ref | gameCode/gameId 누락·오타 | `init`의 `gameCode`가 등록 code와 일치하는지 확인 |
| 429 rate_limited | 1초 미만 간격 연속 제출 | 게임 종료 시점 1회만 제출 |
| 요청 자체가 CORS로 실패(Failed to fetch) | 구버전 SDK 캐시(credentials 문제, 2026-07-20 수정) | 아래 "SDK 캐시" 항목 참고 |
## 6.6 트러블슈팅 — SDK가 최신으로 반영되지 않는 것 같을 때
`/sdk/*.js`는 서버가 `Cache-Control: no-cache`로 내려 **매 로드마다 재검증**하므로
파일을 고치면 즉시 전파됩니다. 그래도 의심되면 순서대로 확인하세요:
1. 콘솔에서 `DevplayRank.version` — 값이 없거나(`undefined`) 옛 날짜면 구버전입니다.
2. 강력 새로고침(Cmd/Ctrl + Shift + R)으로 재확인.
3. 그래도 옛 버전이면 **게임 HTML에 박힌 `?v=...` 버전 쿼리**가 원인입니다. 서버가
재검증을 보장하므로 그 쿼리는 지우는 것이 맞습니다(지우면 항상 최신이 옵니다).
4. 게임이 Vercel/Netlify 등에 있고 그쪽 CDN이 SDK를 다시 캐싱하는 구조라면,
프록시/리라이트로 감싸지 말고 `https://game.dev-play.kr/sdk/rank.js`를 **직접**
참조하세요(원본 서버의 no-cache 헤더가 그대로 적용됩니다).
5. 게임 HTML 자체가 캐시된 경우도 있습니다 — 배포 플랫폼의 HTML 캐시 설정을 확인하세요.
## 6.7 이어서 — 이미 달성한 업적 자동 인증 (셋업 성공 시 필수)
랭킹 연동을 마쳤다면, 이 사용자는 그 과정에서 **이미 여러 업적을 달성한 상태**입니다.
(예: 랭킹 API 연동, 첫 배포, README 작성, GitHub 저장소 생성, .env로 비밀값 분리 등)
사용자가 따로 요청하지 않아도, 위 검증까지 성공했다면 **이어서 아래를 수행하세요.**
1. 업적 목록을 받아 각 업적의 판정 기준과 `verify_method`를 확인합니다:
```bash
curl -sS https://game.dev-play.kr/api/v1/achievements.php \
-H "Authorization: Bearer $DEVPLAY_RANK_TOKEN"
```
2. **지금 작업한 저장소를 실제로 검사해** 이미 충족된 업적을 골라냅니다.
`verify_method`가 `agent`인 것만 대상입니다 — `auto`는 서버가 알아서 감지하고,
`self`는 사용자 경험을 묻는 항목이라 임의로 대신 답하면 안 됩니다.
3. 충족된 업적을 `POST /api/v1/achievements/claim.php`로 제출합니다.
4. 결과를 사용자에게 "이번에 이런 업적이 함께 달성되었어요"로 정리해 알려 주세요.
**전체 판정 기준과 제출 형식은 별도 지침에 있습니다 — 그대로 읽어서 따르세요:**
```
https://game.dev-play.kr/achievements-agent.php?format=md
```
- 이 페이지 URL에 `pair` 값이 있었다면 위 주소에도 `&pair=<같은 값>`을 붙여 같은 토큰을 씁니다.
- **증거 없이 제출하지 마세요.** 저장소에서 실제로 확인한 것만 제출하고,
애매하면 건너뛴 뒤 "이런 업적은 직접 확인이 필요해 보입니다"라고 사용자에게 알리세요.
- 이미 달성한 업적을 다시 제출해도 안전합니다(중복은 서버가 걸러냅니다).
## 7. 엔드포인트 요약
| 메서드/경로 | 인증 | 용도 |
| --- | --- | --- |
| POST /api/v1/games/register.php | Bearer 또는 세션 | 게임 등록/갱신(upsert, code 키) |
| POST /api/v1/scores.php | Bearer 또는 세션 | 점수 제출(best-keeps) |
| GET /api/v1/rankings.php | 공개 | 리더보드 조회 |
| GET /api/v1/me.php | Bearer 또는 세션 | 내 정보 조회 |
**멱등성 원칙**: 이 셋업 전체를 두 번 실행해도 안전합니다.
스크립트 태그 중복 추가만 피하고, 이미 있으면 값만 갱신하세요. 개념 · 인증
개발놀이터 오락실의 랭킹 API는 여러분이 직접 만든 게임에서 이 스터디 사이트의 회원
계정으로 로그인하고, 게임을 등록하고, 점수를 제출하고, 리더보드를 불러올 수 있게 해줍니다.
<script> 한 줄로 붙이는 공통 SDK(sdk/rank.js)를 쓰면 로그인 팝업·자동
등록·랭킹 위젯까지 바로 쓰고, 원한다면 REST 엔드포인트를 직접 호출할 수도 있습니다.
게임은 숫자 gameId(오락실에 등록된 id) 또는 문자열 gameCode(빌드 시점에 id를
모르는 정적 번들용 슬러그, 예: snake) 중 하나로 지목합니다. 자동 등록은
gameCode가 키이므로 외부 게임은 gameCode를 씁니다. 인증도 두 가지입니다 —
외부 사이트(개인 Vercel 배포 등)는 API 토큰을, game.dev-play.kr에 함께 올라간
1st-party 게임은 기존 로그인 세션을 그대로 씁니다(토큰 발급 불필요).
REST 엔드포인트
모든 응답은 JSON입니다. 성공은 { "ok": true, ... },
실패는 { "ok": false, "error": "코드" } 형태이며 적절한 HTTP 상태 코드를 함께 반환합니다.
기준 도메인은 https://game.dev-play.kr 입니다.
게임 자동 등록/갱신 (인증 필요)
외부 게임이 배포 시점 또는 최초 로드에 스스로 오락실에 등록/갱신합니다
(수동 register.php 폼의 자동화 버전). 게임은 code(슬러그)로 지목하며 같은
코드로 여러 번 호출해도 멱등합니다(첫 호출은 생성, 이후는 갱신). 소유자(creator)는
서버가 인증된 회원으로 정하며 본문으로 바꿀 수 없습니다. 인증은 점수 제출과 동일하게 Bearer 토큰(외부)
또는 1st-party 세션(생략)입니다.
POST /api/v1/games/register.php
Authorization: Bearer 3f8c...(64자리) // 외부 사이트만. 1st-party는 생략(세션 쿠키)
Content-Type: application/json
{
"code": "my-tetris", // 필수, 슬러그 /^[a-z0-9][a-z0-9_-]{0,49}$/ — upsert 키
"name": "토비의 테트리스", // 필수, 최대 120자
"game_url": "https://my-app.vercel.app", // 필수, http/https 최대 500자
"description": "클래식 테트리스", // 선택, 최대 1000자
"score_order": "desc", // 선택, desc(기본)|asc
"score_format": "number" // 선택, number(기본)|time|time_ms
}
응답:
{ "ok": true, "action": "created", "game": { "id": 42, "code": "my-tetris", "name": "토비의 테트리스", "game_url": "https://my-app.vercel.app" } }
// 두 번째 호출부터는 action:"updated"
// 다른 회원이 이미 그 code를 소유하면: 409 { "ok": false, "error": "code_taken" }
리더보드 조회 (공개)
인증이 필요 없는 공개 데이터입니다(CORS: *). 게임은
game_id 또는 game_code 중 하나로 지목합니다. limit은
선택이며 기본 50, 최대 100입니다. 순위는 표준 경쟁 순위(동점자는 같은 순위, 다음 순위는 건너뜀)이며,
게임의 점수 방향(score_order)에 맞춰 정렬됩니다(아래 참고).
{
"ok": true,
"game": { "id": 12, "name": "스네이크 러시" },
"rankings": [
{ "rank": 1, "nickname": "토옵이", "score": 1500, "score_type": "score", "created_at": "2026-07-02 14:00:00" },
{ "rank": 2, "nickname": "루나", "score": 1200, "score_type": "score", "created_at": "2026-07-02 15:10:00" }
]
}
점수 제출 (인증 필요)
본문은 JSON이며 게임은 game_id 또는 game_code로
지목합니다. 인증은 두 가지 중 하나입니다:
- 외부 사이트:
Authorization: Bearer <token>헤더. - 1st-party 게임(game.dev-play.kr에 함께 배포, 같은 오리진): 헤더 없이 기존 로그인 세션 쿠키로 제출(SDK가 자동 처리). 토큰 발급 과정이 필요 없습니다.
어느 경우든 서버가 토큰/세션에서 회원을 확인하므로 본문에 회원 id를 보내도 무시됩니다(스푸핑 불가).
POST /api/v1/scores.php
Authorization: Bearer 3f8c...(64자리) // 외부 사이트만. 1st-party는 생략(세션 쿠키)
Content-Type: application/json
{ "game_code": "snake", "score": 1500, "score_type": "score" }
// 또는 { "game_id": 12, "score": 1500 }
응답:
{ "ok": true, "best_score": 1500, "improved": true, "rank": 1 } improved는 이번 제출로 기록이 갱신됐는지를 나타냅니다.
갱신 조건은 게임의 점수 방향을 따릅니다 — 높을수록 좋은 게임은 더 높은 점수, 낮을수록 좋은 게임은 더
낮은 점수일 때만 갱신되고, 그 외에는 기존 기록이 유지되어 improved:false가 됩니다(아래 정책 참고).
내 정보 조회 (인증 필요)
점수 제출과 동일하게 Bearer 토큰(외부) 또는 1st-party 로그인 세션(생략)으로 인증합니다. 토큰이 유효한지 빠르게 확인할 때도 씁니다.
GET /api/v1/me.php
Authorization: Bearer 3f8c...(64자리) // 1st-party는 생략(세션 쿠키)
응답: { "ok": true, "member": { "id": 3, "nickname": "토옵이" } }
토큰/세션이 없거나 만료/무효: 401 { "ok": false, "error": "unauthorized" }
로그인 팝업
SDK가 내부적으로 여는 페이지입니다. 직접 호출할 일은 거의 없지만, 자체 UI를
만든다면 window.open으로 열고 message 이벤트(event.data.type === 'devplay-rank-auth')를
받으면 됩니다. event.origin이 https://game.dev-play.kr인지 반드시 검증하세요.
SDK 메서드
| 메서드 | 설명 |
|---|---|
init(options) | 초기화. gameId 또는 gameCode(둘 중 하나 필수). 자동 등록 옵션: selfRegister(기본 false — true면 최초 로그인 시 이 게임을 오락실에 upsert, gameCode 필요), gameName, gameUrl(생략 시 현재 페이지 주소), description, scoreOrder('desc'|'asc'), scoreFormat('number'|'time'). 그 외: apiBase(생략 시 game.dev-play.kr, ''이면 same-origin), position, theme, autoButton, loginPrompt. 반환값은 DevplayRank 자신. |
registerGame(overrides?) | 게임을 오락실에 등록/갱신(upsert)하고 {ok,action,game}로 resolve. gameCode가 키다. overrides로 name/game_url/description/score_order/score_format를 그때그때 덮어쓸 수 있습니다. 실패 code: unauthorized, code_taken, invalid_*. 보통은 selfRegister:true로 자동 호출되므로 직접 부를 일은 드뭅니다. |
login() | 로그인 팝업을 열고 완료 시 member로 resolve하는 Promise. 실패 code: popup_blocked, popup_closed. selfRegister:true면 로그인 성공 직후 자동 등록도 1회 시도합니다. |
logout() | 저장된 토큰을 지웁니다. |
getMember() | 현재 로그인 회원({id,nickname}) 또는 null로 resolve. |
submitScore(score, scoreType?) | 점수 제출. {ok,best_score,improved,rank}로 resolve. 비로그인(401)이면 기본적으로 점수를 브라우저(localStorage)에 보관하고 "점수 기록을 위해 로그인할까요?" 모달을 띄웁니다 — 로그인하면 보관분을 전송한 뒤 원래 계약대로 resolve, 거절하면 err.code === 'login_declined'로 reject(점수는 보관 유지, 다음 로그인 방문 때 자동 전송). init({loginPrompt:false})면 종전처럼 err.code === 'unauthorized'로 즉시 reject. |
fetchRankings(limit?) | 리더보드 데이터로 resolve(공개, 토큰 불필요). |
flushPending() | 보관 중인 비로그인 점수를 즉시 재전송 시도. {submitted, remaining}으로 resolve. 페이지 로드 시(로그인 상태면) 자동으로도 실행됩니다. |
open() / close() | 랭킹 패널을 프로그램적으로 열고 닫습니다. rankButton:false로 기본 버튼을 숨겨도 그대로 동작합니다. |
version | 로드된 SDK 버전 문자열. 캐시 문제를 의심할 때 콘솔에서 확인합니다. |
공통 버튼 3종 제어
SDK 한 줄이면 랭킹 🏆(우하단) · 사운드 🔊(우상단) · 나가기 ✕(좌상단) 버튼이 모두
자동으로 생깁니다. 게임에 같은 기능이 이미 있으면 해당 버튼만 끄거나, 게임의 기존 버튼에 SDK 동작을 연결하세요.
각 옵션 값은 true(기본, 표시) / false(숨김) / 'CSS 셀렉터'(내 버튼에 연결) 셋 중 하나입니다.
| 버튼 | init 옵션 | 직접 호출용 API |
|---|---|---|
| 🏆 랭킹 | rankButton (구 autoButton) | DevplayRank.open() |
| 🔊 사운드 | volumeButton (구 data-volume="off") | DevplayVolume.toggle() |
| ✕ 나가기 | exitButton (구 data-exit="off") | DevplayExit.open() |
DevplayRank.init({
gameCode: 'my-tetris',
volumeButton: false, // 인게임 사운드 토글이 이미 있음 → 공통 버튼 숨김
rankButton: '#myRankBtn', // 내가 디자인한 버튼에 랭킹 패널 열기를 연결
exitButton: true // 나가기 버튼은 기본값 그대로 표시
}); 셀렉터를 주면 기본 버튼은 자동으로 숨겨지고 그 요소의 클릭에 동작이 연결됩니다.
이벤트 위임 방식이라 버튼을 나중에 만들거나 다시 그려도 계속 동작합니다. 사운드 버튼에 셀렉터를 주면
클릭=음소거 토글, 길게 누르기=볼륨 fader까지 연결되고 현재 상태가 그 요소에 data-dp-muted="1|0"으로
반영됩니다(CSS로 아이콘 전환에 활용하세요). 레거시 data-exit/data-volume 속성도 계속 동작합니다.
자동 적용 — 오디오 언락 · 더블탭 줌 차단
아래 두 가지는 SDK를 넣는 것만으로 적용되므로 게임에서 따로 만들 필요가 없습니다.
- 오디오 자동재생 정책 해제: 브라우저는 사용자 제스처 없이 소리를 재생하지 못하게 막습니다.
SDK는 화면 아무 곳의 첫 클릭/터치/키입력을 명시적 동의로 보고 모든
AudioContext를resume()하고 무음 버퍼를 1회 재생해(iOS Safari 대응) 오디오를 엽니다. 보조 API:DevplayAudio.isUnlocked(),DevplayAudio.onUnlock(fn),DevplayAudio.register(ctx). - 더블탭 줌 차단: 모바일에서 버튼 연타 시 화면이 확대되는 기본 동작을 막습니다
(
touch-action: manipulation— 더블탭 줌만 차단하고 핀치 줌은 유지). 범위 조절:init({ doubleTapZoom: 'page' })(문서 전체) 또는'allow'(차단 안 함).<meta viewport>의user-scalable=no는 iOS가 무시하고 접근성을 해치므로 쓰지 마세요.
SDK 캐시
/sdk/*.js는 서버가 Cache-Control: no-cache로 내려
매 로드마다 재검증합니다(변경 없으면 304, 수백 바이트). 따라서 스크립트 태그에
?v=... 버전 쿼리를 붙일 필요가 없고, SDK 수정은 즉시 모든 게임에 전파됩니다.
반영이 의심되면 콘솔에서 DevplayRank.version으로 로드된 버전을 확인하세요.
외부 게임 표준 초기화 (권장)
개인 Vercel 배포 게임은 gameCode + selfRegister:true로
초기화하면 로그인 시 자동 등록되고, 저장소에 비밀키가 필요 없습니다.
<script src="https://game.dev-play.kr/sdk/rank.js"></script>
<script>
DevplayRank.init({
gameCode: 'my-tetris',
gameName: '토비의 테트리스',
selfRegister: true
});
function onGameOver(finalScore) {
DevplayRank.submitScore(finalScore, 'score')
.then(function (res) { console.log('내 최고점', res.best_score, '순위', res.rank); });
}
</script>
테마 커스터마이즈
DevplayRank.init({
gameCode: 'my-tetris',
selfRegister: true,
position: 'bottom-left', // bottom-right(기본) | bottom-left | top-right | top-left
theme: {
accentColor: '#ff5a5f',
background: '#ffffff',
textColor: '#161616',
zIndex: 9999
}
});
헤드리스(내 UI 직접 만들기)
기본 버튼/패널 없이 API 메서드만 쓰려면 autoButton: false로 초기화하세요.
DevplayRank.init({ gameCode: 'my-tetris', selfRegister: true, autoButton: false });
// 내 버튼에 직접 연결
myLoginBtn.onclick = function () { DevplayRank.login(); };
myBoardBtn.onclick = function () {
DevplayRank.fetchRankings(10).then(function (data) {
renderMyOwnLeaderboard(data.rankings);
});
};
1st-party (same-origin) 게임 초기화
game.dev-play.kr에 함께 배포된 게임은 apiBase: ''와
gameCode로 초기화하면 됩니다. 로그인 세션을 그대로 쓰므로 팝업 로그인이 필요 없습니다.
DevplayRank.init({ gameCode: 'snake', apiBase: '' });
// 게임 종료 시 — 이미 오락실에 로그인돼 있으면 세션으로 바로 제출된다
DevplayRank.submitScore(1500, 'score'); 최고 기록 유지 정책 + 점수 방향
한 회원은 게임마다 기록 한 줄만 갖습니다. 새로 제출한 점수가 기존 기록보다
더 좋을 때만 갱신되고(달성 시각도 그 시점으로 갱신), 그 외에는 기존 기록이 그대로
유지됩니다. 따라서 게임이 끝날 때마다 부담 없이 매번 submitScore를 호출해도 됩니다 —
서버가 알아서 가장 좋은 기록만 남깁니다.
"더 좋음"의 방향은 게임마다 등록된 점수 방향(score_order)을 따릅니다:
desc(기본, 높을수록 좋음): 점수/스테이지처럼 클수록 상위. 최댓값을 유지합니다.asc(낮을수록 좋음): 클리어 타임/이동 수처럼 작을수록 상위(예: 0h "Oh hi"). 최솟값을 유지합니다.
점수 방향은 게임 등록 시 정해지는 값이며(register.php 폼 또는 자동 등록의
score_order), 제출 시 클라이언트가 바꿀 수 없습니다. 리더보드 정렬과 순위 계산도 이 방향을 따릅니다.
자주 묻는 질문
- 정말 AI에게 프롬프트만 주면 끝인가요?
- 네. 맨 위 "프롬프트 복사" 버튼으로 복사해 Claude Code/Codex에 붙여넣으면,
AI가
?format=md기계용 지침을 읽고 게임 등록·SDK 추가·점수 제출 연결·검증까지 처리합니다. 로그인 상태에서 복사하면 지침 주소에 내 API 토큰(pair)까지 포함되어 소유권 확정도 AI가 알아서 끝냅니다. - 게임 자동 등록은 어떻게 이뤄지나요?
selfRegister:true로 초기화하면, 이 게임에서 처음 로그인하는 순간 SDK가POST /api/v1/games/register.php로 게임을 등록/갱신합니다. 저장소에 토큰이 필요 없습니다. 배포/CI 단계에서 등록하려면 토큰을 발급받아curl로 같은 엔드포인트를 호출하면 됩니다.game_code는 누가 소유하나요?- 먼저 등록한 회원이 소유합니다. 그래서 맨 위 프롬프트(pair 토큰 포함)로 셋업하면 AI가
셋업 시점에 서버측 등록을 1회 실행해 소유권을 내 계정으로 즉시 확정합니다.
다른 회원이 이미 그 코드를 소유했다면 등록은
409 code_taken이 됩니다. - API 토큰을 저장소에 넣어도 되나요?
- 안 됩니다. 토큰은 Vercel 환경변수(
DEVPLAY_RANK_TOKEN)에 저장하고 코드에는 하드코딩하지 마세요. 애초에 클라이언트 자동 등록(selfRegister)을 쓰면 토큰 자체가 필요 없어 가장 안전합니다. - 다른 도메인(내 Vercel 사이트)에서 호출해도 되나요?
- 네. 리더보드 조회는 모든 오리진에 열려 있고(
*), 등록/점수 제출/내 정보 조회는 요청한 오리진을 그대로 허용합니다. 별도의 CORS 설정은 필요 없습니다. game_id와game_code중 무엇을 쓰나요?- 외부 게임은
game_code를 쓰세요(자동 등록의 키). 코드 형식은 소문자 영숫자로 시작하는 최대 50자 (/^[a-z0-9][a-z0-9_-]{0,49}$/)입니다. 점수 제출/조회에서 둘 다 보내면game_id가 우선합니다. - 토큰은 얼마나 유효한가요?
- 사실상 만료 걱정이 없습니다(10년). 토큰 값은 로그인하면 이 페이지에서 언제든
다시 확인할 수 있어 따로 관리할 필요가 없습니다. 혹시 401이 나오면 다시
login()을 호출하거나 이 페이지를 새로고침하세요. - 점수를 너무 자주 보내면 어떻게 되나요?
- 같은 토큰으로 1초에 1번을 초과해 제출하면
429 rate_limited가 반환됩니다. 게임 종료 시점에만 제출하면 문제되지 않습니다. - 점수는 정수만 되나요?
- 네. 0 이상의 정수(최대 약 922경, BIGINT 범위)만 허용합니다. 소수/음수/문자는
400으로 거부됩니다. 클리어 타임처럼 낮을수록 좋은 지표는scoreOrder:'asc'로 등록하고 초 단위 정수를 그대로 제출하세요 (scoreFormat:'time'이면 랭킹에 "N분 N초"로 표시됩니다).