KPlay API
게임과 KPlay 플랫폼 간 통신을 위한 API입니다. 게임은 iframe 안에서 실행되며, KPlayAPI 클래스를 통해 크레딧 결제, 리더보드, 유저 정보 등을 사용할 수 있습니다.
KPlayAPI 클래스
아래 코드를 게임에 복사하세요. 모든 플랫폼 기능이 포함되어 있습니다.
class KPlayAPI {
constructor() {
this.user = { userId: null, name: null, credits: 0 };
this._pending = null;
this._onUserInfo = null;
this._onPurchaseResult = null;
this._onScoreSubmitted = null;
this._reqs = new Map();
this._reqSeq = 0;
this._storagePrefix = 'kplay:';
this.storage = {
getItem: (key) => this._storageGet(key),
setItem: (key, value) => this._storageSet(key, value),
removeItem: (key) => this._storageRemove(key),
keys: () => this._storageKeys(),
};
window.addEventListener('message', (e) => {
const { type, ...data } = e.data ?? {};
if (!type) return;
switch (type) {
case 'user-info':
this.user = { userId: data.userId, name: data.name, credits: data.credits ?? 0 };
this._onUserInfo?.(this.user);
// 로그인되면 로컬 캐시를 서버 데이터로 동기화
if (data.userId) this._syncStorageFromServer();
break;
case 'purchase-result':
if (data.success) this.user.credits = data.balance;
this._onPurchaseResult?.(data);
this._pending = null;
break;
case 'score-submitted':
this._onScoreSubmitted?.(data);
break;
case 'storage-load-result':
case 'storage-set-result':
case 'storage-remove-result': {
const req = this._reqs.get(data.requestId);
if (!req) break;
this._reqs.delete(data.requestId);
data.success ? req.resolve(data) : req.reject(new Error(data.error || 'storage error'));
break;
}
}
});
}
_request(type, payload) {
const requestId = `r${++this._reqSeq}`;
return new Promise((resolve, reject) => {
this._reqs.set(requestId, { resolve, reject });
window.parent.postMessage({ type, requestId, ...payload }, '*');
setTimeout(() => {
if (this._reqs.has(requestId)) {
this._reqs.delete(requestId);
reject(new Error('timeout'));
}
}, 10000);
});
}
_lsKey(key) { return this._storagePrefix + key; }
async _storageGet(key) {
const local = localStorage.getItem(this._lsKey(key));
return local; // 서버 값은 user-info 시점에 이미 로컬로 동기화됨
}
async _storageSet(key, value) {
localStorage.setItem(this._lsKey(key), String(value));
if (!this.user.userId) return; // 비로그인: 로컬만
try { await this._request('storage-set', { key, value: String(value) }); }
catch (e) { console.warn('[kplay] cloud save failed, kept local:', e.message); }
}
async _storageRemove(key) {
localStorage.removeItem(this._lsKey(key));
if (!this.user.userId) return;
try { await this._request('storage-remove', { key }); } catch {}
}
async _storageKeys() {
const keys = [];
const prefix = this._storagePrefix;
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k && k.startsWith(prefix)) keys.push(k.slice(prefix.length));
}
return keys;
}
async _syncStorageFromServer() {
try {
const res = await this._request('storage-load', {});
const data = res.data || {};
// 서버 우선: 서버 값으로 로컬 덮어쓰기
for (const [k, v] of Object.entries(data)) {
localStorage.setItem(this._lsKey(k), v);
}
// 로컬에만 있는 키는 서버로 업로드 (최초 로그인 마이그레이션)
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (!k || !k.startsWith(this._storagePrefix)) continue;
const key = k.slice(this._storagePrefix.length);
if (!(key in data)) {
const value = localStorage.getItem(k);
if (value != null) {
this._request('storage-set', { key, value }).catch(() => {});
}
}
}
} catch (e) {
console.warn('[kplay] storage sync failed:', e.message);
}
}
/** 아이템 구매 요청 */
purchase(itemId, itemName, price) {
if (this._pending) return;
if (!this.user.userId) { this.login(); return; }
if (this.user.credits < price) { this.openCreditShop(); return; }
this._pending = { itemId, itemName, price };
window.parent.postMessage({ type: 'purchase-item', itemId, itemName, price }, '*');
}
/** 점수 제출 */
submitScore(score) {
window.parent.postMessage({ type: 'submit-score', score }, '*');
}
/** 크레딧 충전 모달 열기 */
openCreditShop() {
window.parent.postMessage({ type: 'credit-shop' }, '*');
}
/** 랭킹창 표시 */
showLeaderboard() {
window.parent.postMessage({ type: 'show-leaderboard' }, '*');
}
/** 커스텀 이벤트 로그 */
log(eventType, metadata) {
window.parent.postMessage({ type: 'analytics-event', eventType, metadata }, '*');
}
/** 로그인 요청 */
login() {
window.parent.postMessage({ type: 'login-required' }, '*');
}
/** @deprecated `login()` 사용 */
requestLogin() { this.login(); }
/**
* 전면 광고 (인터스티셜) 표시.
* 레벨 전환, 재시작 등 자연스러운 끊김 지점에서 호출하세요.
* @param {string} adType - 'start' | 'pause' | 'next' | 'browse' (기본 'next')
* @param {string} name - 분석용 이름 (선택)
* @returns {Promise<boolean>} 광고가 실제 노출됐으면 true
*/
showInterstitialAd(adType = 'next', name = 'interstitial') {
return new Promise((resolve) => {
const handler = (e) => {
if (e.data?.type !== 'interstitial-ad-result') return;
window.removeEventListener('message', handler);
resolve(e.data.shown);
};
window.addEventListener('message', handler);
window.parent.postMessage({ type: 'show-interstitial-ad', adType, name }, '*');
setTimeout(() => { window.removeEventListener('message', handler); resolve(false); }, 15000);
});
}
/**
* 보상형 광고 표시.
* 유저가 광고를 끝까지 시청하면 포인트가 지급됩니다.
* @returns {Promise<{success: boolean, points?: number, error?: string}>}
*/
showRewardedAd() {
return new Promise((resolve) => {
const handler = (e) => {
if (e.data?.type !== 'rewarded-ad-result') return;
window.removeEventListener('message', handler);
resolve(e.data);
};
window.addEventListener('message', handler);
window.parent.postMessage({ type: 'show-rewarded-ad' }, '*');
setTimeout(() => { window.removeEventListener('message', handler); resolve({ success: false, error: 'timeout' }); }, 30000);
});
}
/** 이벤트 리스너 등록 */
on(event, callback) {
if (event === 'userInfo') this._onUserInfo = callback;
if (event === 'purchaseResult') this._onPurchaseResult = callback;
if (event === 'scoreSubmitted') this._onScoreSubmitted = callback;
return this;
}
}
사용 예시
const kplay = new KPlayAPI();
// 유저 정보 수신
kplay.on('userInfo', (user) => {
console.log(`${user.name} (크레딧: ${user.credits})`);
});
// 구매 결과 처리
kplay.on('purchaseResult', (result) => {
if (result.success) {
grantItem(result.transactionId); // 게임 내 아이템 지급
} else {
showToast('구매 실패: ' + result.error);
}
});
// 아이템 구매 (로그인/잔액 체크 자동)
kplay.purchase('boost-x2', '2배 부스트', 50);
// 로그인 버튼 클릭 시
kplay.login();
// 점수 제출
kplay.submitScore(15000);
// 랭킹창 열기
kplay.showLeaderboard();
// 커스텀 이벤트 로그
kplay.log('level_complete', { level: 3, time: 42 });
kplay.log('item_used', { itemId: 'potion-hp' });
// 클라우드 스토리지 (localStorage 호환 인터페이스)
// 로그인 상태면 서버에 저장되어 기기 전환해도 유지됨
// 비로그인 상태면 localStorage에만 저장
await kplay.storage.setItem('highscore', '9500');
const score = await kplay.storage.getItem('highscore');
await kplay.storage.removeItem('old-key');
const allKeys = await kplay.storage.keys();
// 전면 광고 (레벨 전환, 재시작 시)
const shown = await kplay.showInterstitialAd('next', 'level-clear');
if (shown) console.log('광고 노출됨');
// 보상형 광고 (유저가 선택해서 시청)
const adResult = await kplay.showRewardedAd();
if (adResult.success) {
console.log(`보상 ${adResult.points} 포인트 지급!`);
}
스토리지 (클라우드 세이브)
kplay.storage는 localStorage와 동일한 key/value 인터페이스입니다. 로그인 상태에선 서버에 저장되어 기기 전환해도 데이터가 유지됩니다. 기존 localStorage.setItem('score', '100') 코드를 await kplay.storage.setItem('score', '100')로 바꾸기만 하면 됩니다.
동작
- 로그인: 서버 + localStorage 동시 저장 (로컬은 오프라인/지연 캐시)
- 비로그인: localStorage만 저장
- 로그인 순간: 서버 값을 우선 로컬에 덮어쓰고, 로컬에만 있던 키는 서버로 업로드 (최초 마이그레이션)
제한
- key 길이 ≤ 128자
- value 크기 ≤ 10KB
- 게임당 총 100 keys / 100KB
API 메서드
| 메서드 | 설명 |
|---|---|
purchase(itemId, itemName, price) | 크레딧으로 아이템 구매. 미로그인 시 로그인 유도, 잔액 부족 시 충전 모달 자동 처리 |
submitScore(score) | 리더보드에 점수 제출 |
showLeaderboard() | 랭킹창 표시 |
log(eventType, metadata?) | 커스텀 이벤트 로그. Analytics에서 확인 가능 |
openCreditShop() | 크레딧 충전 모달 열기 |
login() | 로그인 요청 (Google OAuth 리다이렉트) |
showInterstitialAd(adType?, name?) | 전면 광고 표시. adType: 'start' | 'pause' | 'next' | 'browse'. 광고 노출 여부 반환 |
showRewardedAd() | 보상형 광고 표시. 시청 완료 시 포인트 지급 |
storage.setItem(key, value) | 클라우드 세이브: 값 저장 (로그인 시 서버 동기화) |
storage.getItem(key) | 클라우드 세이브: 값 조회 |
storage.removeItem(key) | 클라우드 세이브: 값 삭제 |
storage.keys() | 클라우드 세이브: 저장된 키 목록 |
이벤트
| 이벤트 | 콜백 데이터 | 설명 |
|---|---|---|
userInfo | { userId, name, credits } | 게임 로드 시 자동 수신 |
purchaseResult | { success, transactionId?, balance?, error? } | 구매 결과 |
scoreSubmitted | { success, rank? } | 점수 제출 결과 |
크레딧 시스템
| 팩 | 가격 |
|---|---|
| 100 크레딧 | $0.99 |
| 500 크레딧 | $3.99 |
| 1,000 크레딧 | $6.99 |
서버사이드 웹훅
백엔드가 있는 게임은 웹훅 연동으로 결제를 서버에서 검증할 수 있습니다.