connectbase-client 0.16.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/connect-base.umd.js +3 -3
- package/dist/index.d.mts +177 -42
- package/dist/index.d.ts +177 -42
- package/dist/index.js +235 -92
- package/dist/index.mjs +235 -92
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -61,8 +61,21 @@ var HttpClient = class {
|
|
|
61
61
|
this.refreshPromise = null;
|
|
62
62
|
this.config = { ...config };
|
|
63
63
|
this.storageKey = this.buildStorageKey();
|
|
64
|
+
this.warnIfUnsafePersistence();
|
|
64
65
|
this.restoreTokens();
|
|
65
66
|
}
|
|
67
|
+
warnIfUnsafePersistence() {
|
|
68
|
+
if (typeof window === "undefined") return;
|
|
69
|
+
if (this.config.persistence === "localStorage") {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[connect-base-client] persistence="localStorage" \uB294 XSS \uC2DC \uD1A0\uD070 \uC601\uAD6C \uD0C8\uCDE8 \uC704\uD5D8\uC774 \uC788\uC2B5\uB2C8\uB2E4. refresh token \uC740 \uC11C\uBC84 HttpOnly \uCFE0\uD0A4\uB85C \uBC1C\uAE09\uBC1B\uACE0 \uAE30\uBCF8\uAC12('none')\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.`
|
|
72
|
+
);
|
|
73
|
+
} else if (this.config.persistence === "sessionStorage") {
|
|
74
|
+
console.warn(
|
|
75
|
+
'[connect-base-client] persistence="sessionStorage" \uB294 XSS \uC2DC \uD604\uC7AC \uD0ED \uC138\uC158 \uD0C8\uCDE8 \uC704\uD5D8\uC774 \uC788\uC2B5\uB2C8\uB2E4.'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
66
79
|
updateConfig(config) {
|
|
67
80
|
this.config = { ...this.config, ...config };
|
|
68
81
|
}
|
|
@@ -78,7 +91,7 @@ var HttpClient = class {
|
|
|
78
91
|
}
|
|
79
92
|
// ===== Token Persistence =====
|
|
80
93
|
get persistence() {
|
|
81
|
-
return this.config.persistence ?? "
|
|
94
|
+
return this.config.persistence ?? "none";
|
|
82
95
|
}
|
|
83
96
|
getStorage() {
|
|
84
97
|
if (typeof window === "undefined") return null;
|
|
@@ -86,6 +99,13 @@ var HttpClient = class {
|
|
|
86
99
|
if (this.persistence === "sessionStorage" && typeof sessionStorage !== "undefined") return sessionStorage;
|
|
87
100
|
return null;
|
|
88
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* AuthAPI 등 내부 사용자가 "현재 persistence 설정된 스토리지"를 확인할 수 있도록 노출.
|
|
104
|
+
* persistence='none' 이면 null. XSS 완화 기본값을 공유하기 위함.
|
|
105
|
+
*/
|
|
106
|
+
getPersistenceStorage() {
|
|
107
|
+
return this.getStorage();
|
|
108
|
+
}
|
|
89
109
|
buildStorageKey() {
|
|
90
110
|
const credential = this.config.publicKey ?? this.config.secretKey;
|
|
91
111
|
if (!credential) return TOKEN_STORAGE_KEY;
|
|
@@ -164,6 +184,12 @@ var HttpClient = class {
|
|
|
164
184
|
getAccessToken() {
|
|
165
185
|
return this.config.accessToken;
|
|
166
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* JWT(Access Token) 가 설정되어 있는지 확인
|
|
189
|
+
*/
|
|
190
|
+
hasJWT() {
|
|
191
|
+
return !!this.config.accessToken;
|
|
192
|
+
}
|
|
167
193
|
/**
|
|
168
194
|
* Base URL 반환
|
|
169
195
|
*/
|
|
@@ -347,10 +373,10 @@ function notifyVisitorTracker(memberId) {
|
|
|
347
373
|
}
|
|
348
374
|
}
|
|
349
375
|
}
|
|
350
|
-
function
|
|
376
|
+
function credentialToStorageKeyHash(credential) {
|
|
351
377
|
let hash = 0;
|
|
352
|
-
for (let i = 0; i <
|
|
353
|
-
const char =
|
|
378
|
+
for (let i = 0; i < credential.length; i++) {
|
|
379
|
+
const char = credential.charCodeAt(i);
|
|
354
380
|
hash = (hash << 5) - hash + char;
|
|
355
381
|
hash = hash & hash;
|
|
356
382
|
}
|
|
@@ -506,8 +532,9 @@ var AuthAPI = class {
|
|
|
506
532
|
* 저장된 게스트 멤버 토큰 삭제
|
|
507
533
|
*/
|
|
508
534
|
clearGuestMemberTokens() {
|
|
509
|
-
|
|
510
|
-
|
|
535
|
+
const storage = this.http.getPersistenceStorage();
|
|
536
|
+
if (!storage) return;
|
|
537
|
+
storage.removeItem(this.getGuestMemberTokenKey());
|
|
511
538
|
}
|
|
512
539
|
// ===== Private Methods =====
|
|
513
540
|
async executeGuestMemberLogin() {
|
|
@@ -581,14 +608,15 @@ var AuthAPI = class {
|
|
|
581
608
|
if (!credential) {
|
|
582
609
|
this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}default`;
|
|
583
610
|
} else {
|
|
584
|
-
const keyHash =
|
|
611
|
+
const keyHash = credentialToStorageKeyHash(credential);
|
|
585
612
|
this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}${keyHash}`;
|
|
586
613
|
}
|
|
587
614
|
return this.cachedGuestMemberTokenKey;
|
|
588
615
|
}
|
|
589
616
|
getStoredGuestMemberTokens() {
|
|
590
|
-
|
|
591
|
-
|
|
617
|
+
const storage = this.http.getPersistenceStorage();
|
|
618
|
+
if (!storage) return null;
|
|
619
|
+
const stored = storage.getItem(this.getGuestMemberTokenKey());
|
|
592
620
|
if (!stored) return null;
|
|
593
621
|
try {
|
|
594
622
|
return JSON.parse(stored);
|
|
@@ -597,8 +625,9 @@ var AuthAPI = class {
|
|
|
597
625
|
}
|
|
598
626
|
}
|
|
599
627
|
storeGuestMemberTokens(accessToken, refreshToken, memberId) {
|
|
600
|
-
|
|
601
|
-
|
|
628
|
+
const storage = this.http.getPersistenceStorage();
|
|
629
|
+
if (!storage) return;
|
|
630
|
+
storage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
|
|
602
631
|
}
|
|
603
632
|
};
|
|
604
633
|
|
|
@@ -1856,21 +1885,33 @@ var StorageAPI = class {
|
|
|
1856
1885
|
}
|
|
1857
1886
|
/**
|
|
1858
1887
|
* 파일/폴더 이동
|
|
1888
|
+
*
|
|
1889
|
+
* Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
|
|
1859
1890
|
*/
|
|
1860
1891
|
async moveFile(storageId, fileId, data) {
|
|
1861
|
-
|
|
1892
|
+
if (this.http.hasPublicKey() && !this.http.hasJWT()) {
|
|
1893
|
+
throw new Error(
|
|
1894
|
+
"storage.moveFile requires JWT (console token) auth; not available with Public Key alone."
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1862
1897
|
await this.http.post(
|
|
1863
|
-
|
|
1898
|
+
`/v1/storages/files/${storageId}/items/${fileId}/move`,
|
|
1864
1899
|
data
|
|
1865
1900
|
);
|
|
1866
1901
|
}
|
|
1867
1902
|
/**
|
|
1868
1903
|
* 파일/폴더 이름 변경
|
|
1904
|
+
*
|
|
1905
|
+
* Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
|
|
1869
1906
|
*/
|
|
1870
1907
|
async renameFile(storageId, fileId, data) {
|
|
1871
|
-
|
|
1908
|
+
if (this.http.hasPublicKey() && !this.http.hasJWT()) {
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
"storage.renameFile requires JWT (console token) auth; not available with Public Key alone."
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1872
1913
|
return this.http.patch(
|
|
1873
|
-
|
|
1914
|
+
`/v1/storages/files/${storageId}/items/${fileId}/rename`,
|
|
1874
1915
|
data
|
|
1875
1916
|
);
|
|
1876
1917
|
}
|
|
@@ -3303,7 +3344,10 @@ var WebRTCAPI = class {
|
|
|
3303
3344
|
* ICE 서버 목록 조회
|
|
3304
3345
|
*/
|
|
3305
3346
|
async getICEServers() {
|
|
3306
|
-
|
|
3347
|
+
if (!this.appId) {
|
|
3348
|
+
throw new Error("WebRTC getICEServers \uC5D0\uB294 appId \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. ConnectBase \uCD08\uAE30\uD654 \uC2DC appId \uB97C \uC124\uC815\uD558\uC138\uC694.");
|
|
3349
|
+
}
|
|
3350
|
+
const response = await this.http.get(`/v1/apps/${this.appId}/ice-servers`);
|
|
3307
3351
|
this.iceServers = response.ice_servers;
|
|
3308
3352
|
return response.ice_servers;
|
|
3309
3353
|
}
|
|
@@ -3774,13 +3818,13 @@ var WebRTCAPI = class {
|
|
|
3774
3818
|
* 앱의 WebRTC 통계 조회
|
|
3775
3819
|
*/
|
|
3776
3820
|
async getStats(appID) {
|
|
3777
|
-
return this.http.get(`/v1/apps/${appID}/
|
|
3821
|
+
return this.http.get(`/v1/apps/${appID}/stats`);
|
|
3778
3822
|
}
|
|
3779
3823
|
/**
|
|
3780
3824
|
* 앱의 활성 룸 목록 조회
|
|
3781
3825
|
*/
|
|
3782
3826
|
async getRooms(appID) {
|
|
3783
|
-
return this.http.get(`/v1/apps/${appID}/
|
|
3827
|
+
return this.http.get(`/v1/apps/${appID}/rooms`);
|
|
3784
3828
|
}
|
|
3785
3829
|
};
|
|
3786
3830
|
|
|
@@ -4219,6 +4263,43 @@ var OAuthAPI = class {
|
|
|
4219
4263
|
this.http.setTokens(accessToken, refreshToken);
|
|
4220
4264
|
return result;
|
|
4221
4265
|
}
|
|
4266
|
+
/**
|
|
4267
|
+
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
4268
|
+
*
|
|
4269
|
+
* 서버가 `OAUTH_CODE_ONLY=true` 모드면 리다이렉트 URL 에 토큰 대신 `code` 만 포함됩니다.
|
|
4270
|
+
* 이 메서드는 code 를 `POST /v1/auth/oauth/exchange` 로 교환하고 토큰을 자동 저장합니다.
|
|
4271
|
+
*
|
|
4272
|
+
* 권장 호출 순서:
|
|
4273
|
+
* 1) `cb.oauth.exchangeCodeFromCallback()` 먼저 — code-only 모드면 성공
|
|
4274
|
+
* 2) null 반환 시 `cb.oauth.getCallbackResult()` 폴백 (legacy 토큰-in-URL 모드)
|
|
4275
|
+
*
|
|
4276
|
+
* @example
|
|
4277
|
+
* ```typescript
|
|
4278
|
+
* const result = await cb.oauth.exchangeCodeFromCallback()
|
|
4279
|
+
* ?? cb.oauth.getCallbackResult()
|
|
4280
|
+
* ```
|
|
4281
|
+
*/
|
|
4282
|
+
async exchangeCodeFromCallback() {
|
|
4283
|
+
const params = new URLSearchParams(window.location.search);
|
|
4284
|
+
const code = params.get("code");
|
|
4285
|
+
if (!code) return null;
|
|
4286
|
+
if (params.get("error")) return null;
|
|
4287
|
+
const resp = await this.http.post("/v1/auth/oauth/exchange", { code });
|
|
4288
|
+
const result = {
|
|
4289
|
+
access_token: resp.access_token,
|
|
4290
|
+
refresh_token: resp.refresh_token,
|
|
4291
|
+
member_id: resp.member_id,
|
|
4292
|
+
is_new_member: resp.is_new_member,
|
|
4293
|
+
state: params.get("state") || void 0
|
|
4294
|
+
};
|
|
4295
|
+
if (window.opener) {
|
|
4296
|
+
window.opener.postMessage({ type: "oauth-callback", ...result }, "*");
|
|
4297
|
+
window.close();
|
|
4298
|
+
return result;
|
|
4299
|
+
}
|
|
4300
|
+
this.http.setTokens(result.access_token, result.refresh_token);
|
|
4301
|
+
return result;
|
|
4302
|
+
}
|
|
4222
4303
|
};
|
|
4223
4304
|
|
|
4224
4305
|
// src/api/payment.ts
|
|
@@ -4241,38 +4322,67 @@ var PaymentAPI = class {
|
|
|
4241
4322
|
*
|
|
4242
4323
|
* @example
|
|
4243
4324
|
* ```typescript
|
|
4244
|
-
* const
|
|
4325
|
+
* const result = await cb.payment.prepare({
|
|
4245
4326
|
* amount: 10000,
|
|
4246
|
-
* order_name: '
|
|
4327
|
+
* order_name: '프리미엄 1개월',
|
|
4247
4328
|
* customer_email: 'test@example.com',
|
|
4248
4329
|
* customer_name: '홍길동'
|
|
4249
4330
|
* })
|
|
4250
4331
|
*
|
|
4251
|
-
* //
|
|
4252
|
-
*
|
|
4253
|
-
*
|
|
4254
|
-
*
|
|
4255
|
-
*
|
|
4256
|
-
*
|
|
4257
|
-
*
|
|
4258
|
-
*
|
|
4259
|
-
*
|
|
4260
|
-
*
|
|
4332
|
+
* // Toss V2 결제 플로우
|
|
4333
|
+
* if (result.payment_provider === 'toss') {
|
|
4334
|
+
* const tossPayments = TossPayments(result.toss_client_key)
|
|
4335
|
+
* const payment = tossPayments.payment({ customerKey: result.customer_key })
|
|
4336
|
+
* await payment.requestPayment({
|
|
4337
|
+
* method: 'CARD',
|
|
4338
|
+
* amount: { currency: 'KRW', value: result.amount },
|
|
4339
|
+
* orderId: result.order_id,
|
|
4340
|
+
* orderName: result.order_name,
|
|
4341
|
+
* successUrl: result.success_url,
|
|
4342
|
+
* failUrl: result.fail_url,
|
|
4343
|
+
* })
|
|
4344
|
+
* }
|
|
4261
4345
|
*
|
|
4262
|
-
* // Stripe 결제
|
|
4263
|
-
*
|
|
4264
|
-
*
|
|
4265
|
-
*
|
|
4266
|
-
*
|
|
4267
|
-
*
|
|
4268
|
-
*
|
|
4269
|
-
* // }
|
|
4346
|
+
* // Stripe 결제 플로우
|
|
4347
|
+
* if (result.payment_provider === 'stripe') {
|
|
4348
|
+
* const stripe = await loadStripe(result.stripe_publishable_key)
|
|
4349
|
+
* const elements = stripe.elements({ clientSecret: result.stripe_client_secret })
|
|
4350
|
+
* // Mount PaymentElement, then:
|
|
4351
|
+
* // stripe.confirmPayment({ elements, redirect: 'if_required' })
|
|
4352
|
+
* }
|
|
4270
4353
|
* ```
|
|
4271
4354
|
*/
|
|
4272
4355
|
async prepare(data) {
|
|
4273
4356
|
const prefix = this.getPublicPrefix();
|
|
4274
4357
|
return this.http.post(`${prefix}/payments/prepare`, data);
|
|
4275
4358
|
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Stripe-hosted 결제 페이지 세션 생성 (client_secret 미노출 플로우).
|
|
4361
|
+
*
|
|
4362
|
+
* Elements 플로우(`prepare`) 대신 Stripe-hosted Checkout 페이지로 리다이렉트하고 싶을 때 사용.
|
|
4363
|
+
* 응답에 `session_url` 이 포함되며, 브라우저를 해당 URL 로 이동시키면 Stripe 가 카드 입력·결제를 처리한 뒤
|
|
4364
|
+
* `success_url` / `cancel_url` 로 복귀한다. `client_secret` 은 서버·클라이언트 어디에도 노출되지 않는다.
|
|
4365
|
+
*
|
|
4366
|
+
* @param data - 결제 세션 정보 (금액, 통화, 상품명, success/cancel URL 등)
|
|
4367
|
+
* @returns session_id, session_url (redirect 대상)
|
|
4368
|
+
*
|
|
4369
|
+
* @example
|
|
4370
|
+
* ```typescript
|
|
4371
|
+
* const sess = await client.payment.createCheckoutSession({
|
|
4372
|
+
* amount: 1000,
|
|
4373
|
+
* currency: 'USD',
|
|
4374
|
+
* product_name: 'Premium Subscription',
|
|
4375
|
+
* success_url: 'https://app.example.com/success?session_id={CHECKOUT_SESSION_ID}',
|
|
4376
|
+
* cancel_url: 'https://app.example.com/cancel',
|
|
4377
|
+
* customer_email: 'user@example.com',
|
|
4378
|
+
* })
|
|
4379
|
+
* window.location.href = sess.session_url
|
|
4380
|
+
* ```
|
|
4381
|
+
*/
|
|
4382
|
+
async createCheckoutSession(data) {
|
|
4383
|
+
const prefix = this.getPublicPrefix();
|
|
4384
|
+
return this.http.post(`${prefix}/payments/checkout-session`, data);
|
|
4385
|
+
}
|
|
4276
4386
|
/**
|
|
4277
4387
|
* 결제 승인
|
|
4278
4388
|
* 토스에서 결제 완료 후 콜백으로 받은 정보로 결제를 최종 승인합니다.
|
|
@@ -6077,41 +6187,19 @@ var GameAPI = class {
|
|
|
6077
6187
|
}
|
|
6078
6188
|
/**
|
|
6079
6189
|
* 룸 생성 (HTTP, gRPC 대안)
|
|
6190
|
+
*
|
|
6191
|
+
* @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
|
|
6080
6192
|
*/
|
|
6081
|
-
async createRoom(
|
|
6082
|
-
|
|
6083
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms`, {
|
|
6084
|
-
method: "POST",
|
|
6085
|
-
headers: {
|
|
6086
|
-
...this.getHeaders(),
|
|
6087
|
-
"Content-Type": "application/json"
|
|
6088
|
-
},
|
|
6089
|
-
body: JSON.stringify({
|
|
6090
|
-
app_id: appId,
|
|
6091
|
-
category_id: config.categoryId,
|
|
6092
|
-
room_id: config.roomId,
|
|
6093
|
-
tick_rate: config.tickRate,
|
|
6094
|
-
max_players: config.maxPlayers,
|
|
6095
|
-
metadata: config.metadata
|
|
6096
|
-
})
|
|
6097
|
-
});
|
|
6098
|
-
if (!response.ok) {
|
|
6099
|
-
throw new Error(`Failed to create room: ${response.statusText}`);
|
|
6100
|
-
}
|
|
6101
|
-
return response.json();
|
|
6193
|
+
async createRoom(_appId, _config = {}) {
|
|
6194
|
+
throw new Error("cb.game.createRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
|
|
6102
6195
|
}
|
|
6103
6196
|
/**
|
|
6104
6197
|
* 룸 삭제 (HTTP)
|
|
6198
|
+
*
|
|
6199
|
+
* @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
|
|
6105
6200
|
*/
|
|
6106
|
-
async deleteRoom(
|
|
6107
|
-
|
|
6108
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}`, {
|
|
6109
|
-
method: "DELETE",
|
|
6110
|
-
headers: this.getHeaders()
|
|
6111
|
-
});
|
|
6112
|
-
if (!response.ok) {
|
|
6113
|
-
throw new Error(`Failed to delete room: ${response.statusText}`);
|
|
6114
|
-
}
|
|
6201
|
+
async deleteRoom(_roomId) {
|
|
6202
|
+
throw new Error("cb.game.deleteRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
|
|
6115
6203
|
}
|
|
6116
6204
|
// ==================== Matchmaking ====================
|
|
6117
6205
|
/**
|
|
@@ -6511,6 +6599,8 @@ var GameAPI = class {
|
|
|
6511
6599
|
}));
|
|
6512
6600
|
}
|
|
6513
6601
|
// --- Party API ---
|
|
6602
|
+
// 1.1.0 부터 `createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화.
|
|
6603
|
+
// 1.3.0 부터 `acceptPartyInvite/declinePartyInvite` 추가. 직접 join 엔드포인트는 없음 — `joinParty` 는 throw.
|
|
6514
6604
|
async createParty(playerId, maxSize) {
|
|
6515
6605
|
const id = this.appId || "";
|
|
6516
6606
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties`, {
|
|
@@ -6521,16 +6611,49 @@ var GameAPI = class {
|
|
|
6521
6611
|
if (!response.ok) throw new Error(`Failed to create party: ${response.statusText}`);
|
|
6522
6612
|
return response.json();
|
|
6523
6613
|
}
|
|
6524
|
-
|
|
6614
|
+
/**
|
|
6615
|
+
* @deprecated 백엔드에서 직접 join 엔드포인트 미제공 — `inviteToParty` → `acceptPartyInvite` 플로우 사용
|
|
6616
|
+
*/
|
|
6617
|
+
async joinParty(_partyId, _playerId) {
|
|
6618
|
+
throw new Error("cb.game.joinParty is not supported \u2014 use inviteToParty + acceptPartyInvite flow.");
|
|
6619
|
+
}
|
|
6620
|
+
/**
|
|
6621
|
+
* 파티 초대 수락
|
|
6622
|
+
*
|
|
6623
|
+
* `inviteToParty` 로 생성된 초대 ID 를 수락하여 파티에 합류한다.
|
|
6624
|
+
* 로비 초대(`acceptInvite`) 와 다른 엔드포인트 (`/v1/game/:appID/invites/:inviteID/accept`).
|
|
6625
|
+
*/
|
|
6626
|
+
async acceptPartyInvite(inviteId, playerId, displayName) {
|
|
6525
6627
|
const id = this.appId || "";
|
|
6526
|
-
const
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6628
|
+
const qs = new URLSearchParams({ player_id: playerId });
|
|
6629
|
+
if (displayName) qs.set("display_name", displayName);
|
|
6630
|
+
const response = await fetch(
|
|
6631
|
+
`${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/accept?${qs.toString()}`,
|
|
6632
|
+
{ method: "POST", headers: this.getHeaders() }
|
|
6633
|
+
);
|
|
6634
|
+
if (!response.ok) {
|
|
6635
|
+
const err = await response.json().catch(() => ({}));
|
|
6636
|
+
throw new Error(err.error || `Failed to accept party invite: ${response.statusText}`);
|
|
6637
|
+
}
|
|
6532
6638
|
return response.json();
|
|
6533
6639
|
}
|
|
6640
|
+
/**
|
|
6641
|
+
* 파티 초대 거절
|
|
6642
|
+
*
|
|
6643
|
+
* `inviteToParty` 로 생성된 초대 ID 를 거절한다.
|
|
6644
|
+
*/
|
|
6645
|
+
async declinePartyInvite(inviteId, playerId) {
|
|
6646
|
+
const id = this.appId || "";
|
|
6647
|
+
const qs = new URLSearchParams({ player_id: playerId });
|
|
6648
|
+
const response = await fetch(
|
|
6649
|
+
`${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/decline?${qs.toString()}`,
|
|
6650
|
+
{ method: "POST", headers: this.getHeaders() }
|
|
6651
|
+
);
|
|
6652
|
+
if (!response.ok) {
|
|
6653
|
+
const err = await response.json().catch(() => ({}));
|
|
6654
|
+
throw new Error(err.error || `Failed to decline party invite: ${response.statusText}`);
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6534
6657
|
async leaveParty(partyId, playerId) {
|
|
6535
6658
|
const id = this.appId || "";
|
|
6536
6659
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/leave`, {
|
|
@@ -6563,14 +6686,15 @@ var GameAPI = class {
|
|
|
6563
6686
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/chat`, {
|
|
6564
6687
|
method: "POST",
|
|
6565
6688
|
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6566
|
-
body: JSON.stringify({
|
|
6689
|
+
body: JSON.stringify({ sender_id: playerId, content: message })
|
|
6567
6690
|
});
|
|
6568
6691
|
if (!response.ok) throw new Error(`Failed to send party chat: ${response.statusText}`);
|
|
6569
6692
|
}
|
|
6570
6693
|
// --- Spectator API ---
|
|
6694
|
+
// 1.1.0 부터 활성화. 백엔드 엔드포인트는 `/rooms/:roomID/spectators` 를 사용 (기존 `/spectate` 아님).
|
|
6571
6695
|
async joinSpectator(roomId, playerId) {
|
|
6572
6696
|
const id = this.appId || "";
|
|
6573
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/
|
|
6697
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators`, {
|
|
6574
6698
|
method: "POST",
|
|
6575
6699
|
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6576
6700
|
body: JSON.stringify({ player_id: playerId })
|
|
@@ -6580,10 +6704,9 @@ var GameAPI = class {
|
|
|
6580
6704
|
}
|
|
6581
6705
|
async leaveSpectator(roomId, spectatorId) {
|
|
6582
6706
|
const id = this.appId || "";
|
|
6583
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/
|
|
6584
|
-
method: "
|
|
6585
|
-
headers:
|
|
6586
|
-
body: JSON.stringify({ spectator_id: spectatorId })
|
|
6707
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators/${spectatorId}`, {
|
|
6708
|
+
method: "DELETE",
|
|
6709
|
+
headers: this.getHeaders()
|
|
6587
6710
|
});
|
|
6588
6711
|
if (!response.ok) throw new Error(`Failed to leave spectator: ${response.statusText}`);
|
|
6589
6712
|
}
|
|
@@ -6597,27 +6720,30 @@ var GameAPI = class {
|
|
|
6597
6720
|
return data.spectators || [];
|
|
6598
6721
|
}
|
|
6599
6722
|
// --- Ranking/Leaderboard API ---
|
|
6600
|
-
|
|
6723
|
+
// 1.1.0 부터 활성화. 백엔드가 `app_id` + `game_type` 쿼리를 요구하므로 SDK 시그니처에 `gameType` 추가.
|
|
6724
|
+
async getLeaderboard(gameType, top = 100, season = "default") {
|
|
6601
6725
|
const id = this.appId || "";
|
|
6602
|
-
const
|
|
6603
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top
|
|
6726
|
+
const params = new URLSearchParams({ app_id: id, game_type: gameType, season, limit: String(top) });
|
|
6727
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?${params}`, {
|
|
6604
6728
|
headers: this.getHeaders()
|
|
6605
6729
|
});
|
|
6606
6730
|
if (!response.ok) throw new Error(`Failed to get leaderboard: ${response.statusText}`);
|
|
6607
6731
|
const data = await response.json();
|
|
6608
6732
|
return data.entries || [];
|
|
6609
6733
|
}
|
|
6610
|
-
async getPlayerStats(playerId) {
|
|
6734
|
+
async getPlayerStats(playerId, gameType, season = "default") {
|
|
6611
6735
|
const id = this.appId || "";
|
|
6612
|
-
const
|
|
6736
|
+
const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
|
|
6737
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats?${params}`, {
|
|
6613
6738
|
headers: this.getHeaders()
|
|
6614
6739
|
});
|
|
6615
6740
|
if (!response.ok) throw new Error(`Failed to get player stats: ${response.statusText}`);
|
|
6616
6741
|
return response.json();
|
|
6617
6742
|
}
|
|
6618
|
-
async getPlayerRank(playerId) {
|
|
6743
|
+
async getPlayerRank(playerId, gameType, season = "default") {
|
|
6619
6744
|
const id = this.appId || "";
|
|
6620
|
-
const
|
|
6745
|
+
const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
|
|
6746
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank?${params}`, {
|
|
6621
6747
|
headers: this.getHeaders()
|
|
6622
6748
|
});
|
|
6623
6749
|
if (!response.ok) throw new Error(`Failed to get player rank: ${response.statusText}`);
|
|
@@ -6653,11 +6779,16 @@ var GameAPI = class {
|
|
|
6653
6779
|
if (!response.ok) throw new Error(`Failed to set mute: ${response.statusText}`);
|
|
6654
6780
|
}
|
|
6655
6781
|
// --- Replay API ---
|
|
6782
|
+
// 1.1.0 부터 활성화. 백엔드에서 `REPLAY_STORAGE_PATH` 환경변수가 설정된 경우에만 동작.
|
|
6783
|
+
// 미설정 시 404 응답 → SDK 가 명시적 에러를 throw.
|
|
6656
6784
|
async listReplays(roomId) {
|
|
6657
6785
|
const id = this.appId || "";
|
|
6658
6786
|
let url = `${this.gameServerUrl}/v1/game/${id}/replays`;
|
|
6659
6787
|
if (roomId) url += `?room_id=${roomId}`;
|
|
6660
6788
|
const response = await fetch(url, { headers: this.getHeaders() });
|
|
6789
|
+
if (response.status === 404) {
|
|
6790
|
+
throw new Error("cb.game.listReplays: replay storage is not configured on this server (REPLAY_STORAGE_PATH unset).");
|
|
6791
|
+
}
|
|
6661
6792
|
if (!response.ok) throw new Error(`Failed to list replays: ${response.statusText}`);
|
|
6662
6793
|
const data = await response.json();
|
|
6663
6794
|
return data.replays || [];
|
|
@@ -6667,6 +6798,9 @@ var GameAPI = class {
|
|
|
6667
6798
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}`, {
|
|
6668
6799
|
headers: this.getHeaders()
|
|
6669
6800
|
});
|
|
6801
|
+
if (response.status === 404) {
|
|
6802
|
+
throw new Error("cb.game.getReplay: replay not found or replay storage unconfigured.");
|
|
6803
|
+
}
|
|
6670
6804
|
if (!response.ok) throw new Error(`Failed to get replay: ${response.statusText}`);
|
|
6671
6805
|
return response.json();
|
|
6672
6806
|
}
|
|
@@ -6675,6 +6809,9 @@ var GameAPI = class {
|
|
|
6675
6809
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/download`, {
|
|
6676
6810
|
headers: this.getHeaders()
|
|
6677
6811
|
});
|
|
6812
|
+
if (response.status === 404) {
|
|
6813
|
+
throw new Error("cb.game.downloadReplay: replay not found or replay storage unconfigured.");
|
|
6814
|
+
}
|
|
6678
6815
|
if (!response.ok) throw new Error(`Failed to download replay: ${response.statusText}`);
|
|
6679
6816
|
return response.arrayBuffer();
|
|
6680
6817
|
}
|
|
@@ -6683,6 +6820,9 @@ var GameAPI = class {
|
|
|
6683
6820
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/highlights`, {
|
|
6684
6821
|
headers: this.getHeaders()
|
|
6685
6822
|
});
|
|
6823
|
+
if (response.status === 404) {
|
|
6824
|
+
throw new Error("cb.game.getReplayHighlights: replay not found or replay storage unconfigured.");
|
|
6825
|
+
}
|
|
6686
6826
|
if (!response.ok) throw new Error(`Failed to get highlights: ${response.statusText}`);
|
|
6687
6827
|
const data = await response.json();
|
|
6688
6828
|
return data.highlights || [];
|
|
@@ -6713,15 +6853,18 @@ var AdsAPI = class {
|
|
|
6713
6853
|
return this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
6714
6854
|
}
|
|
6715
6855
|
/**
|
|
6716
|
-
* AdSense 연결 상태 확인
|
|
6856
|
+
* AdSense / AdMob 연결 상태 확인 (중첩 구조)
|
|
6717
6857
|
*
|
|
6718
|
-
* @returns
|
|
6858
|
+
* @returns `{ adsense, admob }` 각각 연결 상태·이메일·계정 ID
|
|
6719
6859
|
*
|
|
6720
6860
|
* @example
|
|
6721
6861
|
* ```typescript
|
|
6722
6862
|
* const status = await cb.ads.getConnectionStatus()
|
|
6723
|
-
* if (status.is_connected) {
|
|
6724
|
-
* console.log('연결됨:', status.email)
|
|
6863
|
+
* if (status.adsense.is_connected) {
|
|
6864
|
+
* console.log('AdSense 연결됨:', status.adsense.email, status.adsense.account_id)
|
|
6865
|
+
* }
|
|
6866
|
+
* if (status.admob.is_connected) {
|
|
6867
|
+
* console.log('AdMob 연결됨:', status.admob.account_id, status.admob.publisher_id)
|
|
6725
6868
|
* }
|
|
6726
6869
|
* ```
|
|
6727
6870
|
*/
|