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.mjs
CHANGED
|
@@ -23,8 +23,21 @@ var HttpClient = class {
|
|
|
23
23
|
this.refreshPromise = null;
|
|
24
24
|
this.config = { ...config };
|
|
25
25
|
this.storageKey = this.buildStorageKey();
|
|
26
|
+
this.warnIfUnsafePersistence();
|
|
26
27
|
this.restoreTokens();
|
|
27
28
|
}
|
|
29
|
+
warnIfUnsafePersistence() {
|
|
30
|
+
if (typeof window === "undefined") return;
|
|
31
|
+
if (this.config.persistence === "localStorage") {
|
|
32
|
+
console.warn(
|
|
33
|
+
`[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.`
|
|
34
|
+
);
|
|
35
|
+
} else if (this.config.persistence === "sessionStorage") {
|
|
36
|
+
console.warn(
|
|
37
|
+
'[connect-base-client] persistence="sessionStorage" \uB294 XSS \uC2DC \uD604\uC7AC \uD0ED \uC138\uC158 \uD0C8\uCDE8 \uC704\uD5D8\uC774 \uC788\uC2B5\uB2C8\uB2E4.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
28
41
|
updateConfig(config) {
|
|
29
42
|
this.config = { ...this.config, ...config };
|
|
30
43
|
}
|
|
@@ -40,7 +53,7 @@ var HttpClient = class {
|
|
|
40
53
|
}
|
|
41
54
|
// ===== Token Persistence =====
|
|
42
55
|
get persistence() {
|
|
43
|
-
return this.config.persistence ?? "
|
|
56
|
+
return this.config.persistence ?? "none";
|
|
44
57
|
}
|
|
45
58
|
getStorage() {
|
|
46
59
|
if (typeof window === "undefined") return null;
|
|
@@ -48,6 +61,13 @@ var HttpClient = class {
|
|
|
48
61
|
if (this.persistence === "sessionStorage" && typeof sessionStorage !== "undefined") return sessionStorage;
|
|
49
62
|
return null;
|
|
50
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* AuthAPI 등 내부 사용자가 "현재 persistence 설정된 스토리지"를 확인할 수 있도록 노출.
|
|
66
|
+
* persistence='none' 이면 null. XSS 완화 기본값을 공유하기 위함.
|
|
67
|
+
*/
|
|
68
|
+
getPersistenceStorage() {
|
|
69
|
+
return this.getStorage();
|
|
70
|
+
}
|
|
51
71
|
buildStorageKey() {
|
|
52
72
|
const credential = this.config.publicKey ?? this.config.secretKey;
|
|
53
73
|
if (!credential) return TOKEN_STORAGE_KEY;
|
|
@@ -126,6 +146,12 @@ var HttpClient = class {
|
|
|
126
146
|
getAccessToken() {
|
|
127
147
|
return this.config.accessToken;
|
|
128
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* JWT(Access Token) 가 설정되어 있는지 확인
|
|
151
|
+
*/
|
|
152
|
+
hasJWT() {
|
|
153
|
+
return !!this.config.accessToken;
|
|
154
|
+
}
|
|
129
155
|
/**
|
|
130
156
|
* Base URL 반환
|
|
131
157
|
*/
|
|
@@ -309,10 +335,10 @@ function notifyVisitorTracker(memberId) {
|
|
|
309
335
|
}
|
|
310
336
|
}
|
|
311
337
|
}
|
|
312
|
-
function
|
|
338
|
+
function credentialToStorageKeyHash(credential) {
|
|
313
339
|
let hash = 0;
|
|
314
|
-
for (let i = 0; i <
|
|
315
|
-
const char =
|
|
340
|
+
for (let i = 0; i < credential.length; i++) {
|
|
341
|
+
const char = credential.charCodeAt(i);
|
|
316
342
|
hash = (hash << 5) - hash + char;
|
|
317
343
|
hash = hash & hash;
|
|
318
344
|
}
|
|
@@ -468,8 +494,9 @@ var AuthAPI = class {
|
|
|
468
494
|
* 저장된 게스트 멤버 토큰 삭제
|
|
469
495
|
*/
|
|
470
496
|
clearGuestMemberTokens() {
|
|
471
|
-
|
|
472
|
-
|
|
497
|
+
const storage = this.http.getPersistenceStorage();
|
|
498
|
+
if (!storage) return;
|
|
499
|
+
storage.removeItem(this.getGuestMemberTokenKey());
|
|
473
500
|
}
|
|
474
501
|
// ===== Private Methods =====
|
|
475
502
|
async executeGuestMemberLogin() {
|
|
@@ -543,14 +570,15 @@ var AuthAPI = class {
|
|
|
543
570
|
if (!credential) {
|
|
544
571
|
this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}default`;
|
|
545
572
|
} else {
|
|
546
|
-
const keyHash =
|
|
573
|
+
const keyHash = credentialToStorageKeyHash(credential);
|
|
547
574
|
this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}${keyHash}`;
|
|
548
575
|
}
|
|
549
576
|
return this.cachedGuestMemberTokenKey;
|
|
550
577
|
}
|
|
551
578
|
getStoredGuestMemberTokens() {
|
|
552
|
-
|
|
553
|
-
|
|
579
|
+
const storage = this.http.getPersistenceStorage();
|
|
580
|
+
if (!storage) return null;
|
|
581
|
+
const stored = storage.getItem(this.getGuestMemberTokenKey());
|
|
554
582
|
if (!stored) return null;
|
|
555
583
|
try {
|
|
556
584
|
return JSON.parse(stored);
|
|
@@ -559,8 +587,9 @@ var AuthAPI = class {
|
|
|
559
587
|
}
|
|
560
588
|
}
|
|
561
589
|
storeGuestMemberTokens(accessToken, refreshToken, memberId) {
|
|
562
|
-
|
|
563
|
-
|
|
590
|
+
const storage = this.http.getPersistenceStorage();
|
|
591
|
+
if (!storage) return;
|
|
592
|
+
storage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
|
|
564
593
|
}
|
|
565
594
|
};
|
|
566
595
|
|
|
@@ -1818,21 +1847,33 @@ var StorageAPI = class {
|
|
|
1818
1847
|
}
|
|
1819
1848
|
/**
|
|
1820
1849
|
* 파일/폴더 이동
|
|
1850
|
+
*
|
|
1851
|
+
* Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
|
|
1821
1852
|
*/
|
|
1822
1853
|
async moveFile(storageId, fileId, data) {
|
|
1823
|
-
|
|
1854
|
+
if (this.http.hasPublicKey() && !this.http.hasJWT()) {
|
|
1855
|
+
throw new Error(
|
|
1856
|
+
"storage.moveFile requires JWT (console token) auth; not available with Public Key alone."
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1824
1859
|
await this.http.post(
|
|
1825
|
-
|
|
1860
|
+
`/v1/storages/files/${storageId}/items/${fileId}/move`,
|
|
1826
1861
|
data
|
|
1827
1862
|
);
|
|
1828
1863
|
}
|
|
1829
1864
|
/**
|
|
1830
1865
|
* 파일/폴더 이름 변경
|
|
1866
|
+
*
|
|
1867
|
+
* Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
|
|
1831
1868
|
*/
|
|
1832
1869
|
async renameFile(storageId, fileId, data) {
|
|
1833
|
-
|
|
1870
|
+
if (this.http.hasPublicKey() && !this.http.hasJWT()) {
|
|
1871
|
+
throw new Error(
|
|
1872
|
+
"storage.renameFile requires JWT (console token) auth; not available with Public Key alone."
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1834
1875
|
return this.http.patch(
|
|
1835
|
-
|
|
1876
|
+
`/v1/storages/files/${storageId}/items/${fileId}/rename`,
|
|
1836
1877
|
data
|
|
1837
1878
|
);
|
|
1838
1879
|
}
|
|
@@ -3265,7 +3306,10 @@ var WebRTCAPI = class {
|
|
|
3265
3306
|
* ICE 서버 목록 조회
|
|
3266
3307
|
*/
|
|
3267
3308
|
async getICEServers() {
|
|
3268
|
-
|
|
3309
|
+
if (!this.appId) {
|
|
3310
|
+
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.");
|
|
3311
|
+
}
|
|
3312
|
+
const response = await this.http.get(`/v1/apps/${this.appId}/ice-servers`);
|
|
3269
3313
|
this.iceServers = response.ice_servers;
|
|
3270
3314
|
return response.ice_servers;
|
|
3271
3315
|
}
|
|
@@ -3736,13 +3780,13 @@ var WebRTCAPI = class {
|
|
|
3736
3780
|
* 앱의 WebRTC 통계 조회
|
|
3737
3781
|
*/
|
|
3738
3782
|
async getStats(appID) {
|
|
3739
|
-
return this.http.get(`/v1/apps/${appID}/
|
|
3783
|
+
return this.http.get(`/v1/apps/${appID}/stats`);
|
|
3740
3784
|
}
|
|
3741
3785
|
/**
|
|
3742
3786
|
* 앱의 활성 룸 목록 조회
|
|
3743
3787
|
*/
|
|
3744
3788
|
async getRooms(appID) {
|
|
3745
|
-
return this.http.get(`/v1/apps/${appID}/
|
|
3789
|
+
return this.http.get(`/v1/apps/${appID}/rooms`);
|
|
3746
3790
|
}
|
|
3747
3791
|
};
|
|
3748
3792
|
|
|
@@ -4181,6 +4225,43 @@ var OAuthAPI = class {
|
|
|
4181
4225
|
this.http.setTokens(accessToken, refreshToken);
|
|
4182
4226
|
return result;
|
|
4183
4227
|
}
|
|
4228
|
+
/**
|
|
4229
|
+
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
4230
|
+
*
|
|
4231
|
+
* 서버가 `OAUTH_CODE_ONLY=true` 모드면 리다이렉트 URL 에 토큰 대신 `code` 만 포함됩니다.
|
|
4232
|
+
* 이 메서드는 code 를 `POST /v1/auth/oauth/exchange` 로 교환하고 토큰을 자동 저장합니다.
|
|
4233
|
+
*
|
|
4234
|
+
* 권장 호출 순서:
|
|
4235
|
+
* 1) `cb.oauth.exchangeCodeFromCallback()` 먼저 — code-only 모드면 성공
|
|
4236
|
+
* 2) null 반환 시 `cb.oauth.getCallbackResult()` 폴백 (legacy 토큰-in-URL 모드)
|
|
4237
|
+
*
|
|
4238
|
+
* @example
|
|
4239
|
+
* ```typescript
|
|
4240
|
+
* const result = await cb.oauth.exchangeCodeFromCallback()
|
|
4241
|
+
* ?? cb.oauth.getCallbackResult()
|
|
4242
|
+
* ```
|
|
4243
|
+
*/
|
|
4244
|
+
async exchangeCodeFromCallback() {
|
|
4245
|
+
const params = new URLSearchParams(window.location.search);
|
|
4246
|
+
const code = params.get("code");
|
|
4247
|
+
if (!code) return null;
|
|
4248
|
+
if (params.get("error")) return null;
|
|
4249
|
+
const resp = await this.http.post("/v1/auth/oauth/exchange", { code });
|
|
4250
|
+
const result = {
|
|
4251
|
+
access_token: resp.access_token,
|
|
4252
|
+
refresh_token: resp.refresh_token,
|
|
4253
|
+
member_id: resp.member_id,
|
|
4254
|
+
is_new_member: resp.is_new_member,
|
|
4255
|
+
state: params.get("state") || void 0
|
|
4256
|
+
};
|
|
4257
|
+
if (window.opener) {
|
|
4258
|
+
window.opener.postMessage({ type: "oauth-callback", ...result }, "*");
|
|
4259
|
+
window.close();
|
|
4260
|
+
return result;
|
|
4261
|
+
}
|
|
4262
|
+
this.http.setTokens(result.access_token, result.refresh_token);
|
|
4263
|
+
return result;
|
|
4264
|
+
}
|
|
4184
4265
|
};
|
|
4185
4266
|
|
|
4186
4267
|
// src/api/payment.ts
|
|
@@ -4203,38 +4284,67 @@ var PaymentAPI = class {
|
|
|
4203
4284
|
*
|
|
4204
4285
|
* @example
|
|
4205
4286
|
* ```typescript
|
|
4206
|
-
* const
|
|
4287
|
+
* const result = await cb.payment.prepare({
|
|
4207
4288
|
* amount: 10000,
|
|
4208
|
-
* order_name: '
|
|
4289
|
+
* order_name: '프리미엄 1개월',
|
|
4209
4290
|
* customer_email: 'test@example.com',
|
|
4210
4291
|
* customer_name: '홍길동'
|
|
4211
4292
|
* })
|
|
4212
4293
|
*
|
|
4213
|
-
* //
|
|
4214
|
-
*
|
|
4215
|
-
*
|
|
4216
|
-
*
|
|
4217
|
-
*
|
|
4218
|
-
*
|
|
4219
|
-
*
|
|
4220
|
-
*
|
|
4221
|
-
*
|
|
4222
|
-
*
|
|
4294
|
+
* // Toss V2 결제 플로우
|
|
4295
|
+
* if (result.payment_provider === 'toss') {
|
|
4296
|
+
* const tossPayments = TossPayments(result.toss_client_key)
|
|
4297
|
+
* const payment = tossPayments.payment({ customerKey: result.customer_key })
|
|
4298
|
+
* await payment.requestPayment({
|
|
4299
|
+
* method: 'CARD',
|
|
4300
|
+
* amount: { currency: 'KRW', value: result.amount },
|
|
4301
|
+
* orderId: result.order_id,
|
|
4302
|
+
* orderName: result.order_name,
|
|
4303
|
+
* successUrl: result.success_url,
|
|
4304
|
+
* failUrl: result.fail_url,
|
|
4305
|
+
* })
|
|
4306
|
+
* }
|
|
4223
4307
|
*
|
|
4224
|
-
* // Stripe 결제
|
|
4225
|
-
*
|
|
4226
|
-
*
|
|
4227
|
-
*
|
|
4228
|
-
*
|
|
4229
|
-
*
|
|
4230
|
-
*
|
|
4231
|
-
* // }
|
|
4308
|
+
* // Stripe 결제 플로우
|
|
4309
|
+
* if (result.payment_provider === 'stripe') {
|
|
4310
|
+
* const stripe = await loadStripe(result.stripe_publishable_key)
|
|
4311
|
+
* const elements = stripe.elements({ clientSecret: result.stripe_client_secret })
|
|
4312
|
+
* // Mount PaymentElement, then:
|
|
4313
|
+
* // stripe.confirmPayment({ elements, redirect: 'if_required' })
|
|
4314
|
+
* }
|
|
4232
4315
|
* ```
|
|
4233
4316
|
*/
|
|
4234
4317
|
async prepare(data) {
|
|
4235
4318
|
const prefix = this.getPublicPrefix();
|
|
4236
4319
|
return this.http.post(`${prefix}/payments/prepare`, data);
|
|
4237
4320
|
}
|
|
4321
|
+
/**
|
|
4322
|
+
* Stripe-hosted 결제 페이지 세션 생성 (client_secret 미노출 플로우).
|
|
4323
|
+
*
|
|
4324
|
+
* Elements 플로우(`prepare`) 대신 Stripe-hosted Checkout 페이지로 리다이렉트하고 싶을 때 사용.
|
|
4325
|
+
* 응답에 `session_url` 이 포함되며, 브라우저를 해당 URL 로 이동시키면 Stripe 가 카드 입력·결제를 처리한 뒤
|
|
4326
|
+
* `success_url` / `cancel_url` 로 복귀한다. `client_secret` 은 서버·클라이언트 어디에도 노출되지 않는다.
|
|
4327
|
+
*
|
|
4328
|
+
* @param data - 결제 세션 정보 (금액, 통화, 상품명, success/cancel URL 등)
|
|
4329
|
+
* @returns session_id, session_url (redirect 대상)
|
|
4330
|
+
*
|
|
4331
|
+
* @example
|
|
4332
|
+
* ```typescript
|
|
4333
|
+
* const sess = await client.payment.createCheckoutSession({
|
|
4334
|
+
* amount: 1000,
|
|
4335
|
+
* currency: 'USD',
|
|
4336
|
+
* product_name: 'Premium Subscription',
|
|
4337
|
+
* success_url: 'https://app.example.com/success?session_id={CHECKOUT_SESSION_ID}',
|
|
4338
|
+
* cancel_url: 'https://app.example.com/cancel',
|
|
4339
|
+
* customer_email: 'user@example.com',
|
|
4340
|
+
* })
|
|
4341
|
+
* window.location.href = sess.session_url
|
|
4342
|
+
* ```
|
|
4343
|
+
*/
|
|
4344
|
+
async createCheckoutSession(data) {
|
|
4345
|
+
const prefix = this.getPublicPrefix();
|
|
4346
|
+
return this.http.post(`${prefix}/payments/checkout-session`, data);
|
|
4347
|
+
}
|
|
4238
4348
|
/**
|
|
4239
4349
|
* 결제 승인
|
|
4240
4350
|
* 토스에서 결제 완료 후 콜백으로 받은 정보로 결제를 최종 승인합니다.
|
|
@@ -6039,41 +6149,19 @@ var GameAPI = class {
|
|
|
6039
6149
|
}
|
|
6040
6150
|
/**
|
|
6041
6151
|
* 룸 생성 (HTTP, gRPC 대안)
|
|
6152
|
+
*
|
|
6153
|
+
* @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
|
|
6042
6154
|
*/
|
|
6043
|
-
async createRoom(
|
|
6044
|
-
|
|
6045
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms`, {
|
|
6046
|
-
method: "POST",
|
|
6047
|
-
headers: {
|
|
6048
|
-
...this.getHeaders(),
|
|
6049
|
-
"Content-Type": "application/json"
|
|
6050
|
-
},
|
|
6051
|
-
body: JSON.stringify({
|
|
6052
|
-
app_id: appId,
|
|
6053
|
-
category_id: config.categoryId,
|
|
6054
|
-
room_id: config.roomId,
|
|
6055
|
-
tick_rate: config.tickRate,
|
|
6056
|
-
max_players: config.maxPlayers,
|
|
6057
|
-
metadata: config.metadata
|
|
6058
|
-
})
|
|
6059
|
-
});
|
|
6060
|
-
if (!response.ok) {
|
|
6061
|
-
throw new Error(`Failed to create room: ${response.statusText}`);
|
|
6062
|
-
}
|
|
6063
|
-
return response.json();
|
|
6155
|
+
async createRoom(_appId, _config = {}) {
|
|
6156
|
+
throw new Error("cb.game.createRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
|
|
6064
6157
|
}
|
|
6065
6158
|
/**
|
|
6066
6159
|
* 룸 삭제 (HTTP)
|
|
6160
|
+
*
|
|
6161
|
+
* @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
|
|
6067
6162
|
*/
|
|
6068
|
-
async deleteRoom(
|
|
6069
|
-
|
|
6070
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}`, {
|
|
6071
|
-
method: "DELETE",
|
|
6072
|
-
headers: this.getHeaders()
|
|
6073
|
-
});
|
|
6074
|
-
if (!response.ok) {
|
|
6075
|
-
throw new Error(`Failed to delete room: ${response.statusText}`);
|
|
6076
|
-
}
|
|
6163
|
+
async deleteRoom(_roomId) {
|
|
6164
|
+
throw new Error("cb.game.deleteRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
|
|
6077
6165
|
}
|
|
6078
6166
|
// ==================== Matchmaking ====================
|
|
6079
6167
|
/**
|
|
@@ -6473,6 +6561,8 @@ var GameAPI = class {
|
|
|
6473
6561
|
}));
|
|
6474
6562
|
}
|
|
6475
6563
|
// --- Party API ---
|
|
6564
|
+
// 1.1.0 부터 `createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화.
|
|
6565
|
+
// 1.3.0 부터 `acceptPartyInvite/declinePartyInvite` 추가. 직접 join 엔드포인트는 없음 — `joinParty` 는 throw.
|
|
6476
6566
|
async createParty(playerId, maxSize) {
|
|
6477
6567
|
const id = this.appId || "";
|
|
6478
6568
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties`, {
|
|
@@ -6483,16 +6573,49 @@ var GameAPI = class {
|
|
|
6483
6573
|
if (!response.ok) throw new Error(`Failed to create party: ${response.statusText}`);
|
|
6484
6574
|
return response.json();
|
|
6485
6575
|
}
|
|
6486
|
-
|
|
6576
|
+
/**
|
|
6577
|
+
* @deprecated 백엔드에서 직접 join 엔드포인트 미제공 — `inviteToParty` → `acceptPartyInvite` 플로우 사용
|
|
6578
|
+
*/
|
|
6579
|
+
async joinParty(_partyId, _playerId) {
|
|
6580
|
+
throw new Error("cb.game.joinParty is not supported \u2014 use inviteToParty + acceptPartyInvite flow.");
|
|
6581
|
+
}
|
|
6582
|
+
/**
|
|
6583
|
+
* 파티 초대 수락
|
|
6584
|
+
*
|
|
6585
|
+
* `inviteToParty` 로 생성된 초대 ID 를 수락하여 파티에 합류한다.
|
|
6586
|
+
* 로비 초대(`acceptInvite`) 와 다른 엔드포인트 (`/v1/game/:appID/invites/:inviteID/accept`).
|
|
6587
|
+
*/
|
|
6588
|
+
async acceptPartyInvite(inviteId, playerId, displayName) {
|
|
6487
6589
|
const id = this.appId || "";
|
|
6488
|
-
const
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6590
|
+
const qs = new URLSearchParams({ player_id: playerId });
|
|
6591
|
+
if (displayName) qs.set("display_name", displayName);
|
|
6592
|
+
const response = await fetch(
|
|
6593
|
+
`${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/accept?${qs.toString()}`,
|
|
6594
|
+
{ method: "POST", headers: this.getHeaders() }
|
|
6595
|
+
);
|
|
6596
|
+
if (!response.ok) {
|
|
6597
|
+
const err = await response.json().catch(() => ({}));
|
|
6598
|
+
throw new Error(err.error || `Failed to accept party invite: ${response.statusText}`);
|
|
6599
|
+
}
|
|
6494
6600
|
return response.json();
|
|
6495
6601
|
}
|
|
6602
|
+
/**
|
|
6603
|
+
* 파티 초대 거절
|
|
6604
|
+
*
|
|
6605
|
+
* `inviteToParty` 로 생성된 초대 ID 를 거절한다.
|
|
6606
|
+
*/
|
|
6607
|
+
async declinePartyInvite(inviteId, playerId) {
|
|
6608
|
+
const id = this.appId || "";
|
|
6609
|
+
const qs = new URLSearchParams({ player_id: playerId });
|
|
6610
|
+
const response = await fetch(
|
|
6611
|
+
`${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/decline?${qs.toString()}`,
|
|
6612
|
+
{ method: "POST", headers: this.getHeaders() }
|
|
6613
|
+
);
|
|
6614
|
+
if (!response.ok) {
|
|
6615
|
+
const err = await response.json().catch(() => ({}));
|
|
6616
|
+
throw new Error(err.error || `Failed to decline party invite: ${response.statusText}`);
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6496
6619
|
async leaveParty(partyId, playerId) {
|
|
6497
6620
|
const id = this.appId || "";
|
|
6498
6621
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/leave`, {
|
|
@@ -6525,14 +6648,15 @@ var GameAPI = class {
|
|
|
6525
6648
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/chat`, {
|
|
6526
6649
|
method: "POST",
|
|
6527
6650
|
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6528
|
-
body: JSON.stringify({
|
|
6651
|
+
body: JSON.stringify({ sender_id: playerId, content: message })
|
|
6529
6652
|
});
|
|
6530
6653
|
if (!response.ok) throw new Error(`Failed to send party chat: ${response.statusText}`);
|
|
6531
6654
|
}
|
|
6532
6655
|
// --- Spectator API ---
|
|
6656
|
+
// 1.1.0 부터 활성화. 백엔드 엔드포인트는 `/rooms/:roomID/spectators` 를 사용 (기존 `/spectate` 아님).
|
|
6533
6657
|
async joinSpectator(roomId, playerId) {
|
|
6534
6658
|
const id = this.appId || "";
|
|
6535
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/
|
|
6659
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators`, {
|
|
6536
6660
|
method: "POST",
|
|
6537
6661
|
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6538
6662
|
body: JSON.stringify({ player_id: playerId })
|
|
@@ -6542,10 +6666,9 @@ var GameAPI = class {
|
|
|
6542
6666
|
}
|
|
6543
6667
|
async leaveSpectator(roomId, spectatorId) {
|
|
6544
6668
|
const id = this.appId || "";
|
|
6545
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/
|
|
6546
|
-
method: "
|
|
6547
|
-
headers:
|
|
6548
|
-
body: JSON.stringify({ spectator_id: spectatorId })
|
|
6669
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators/${spectatorId}`, {
|
|
6670
|
+
method: "DELETE",
|
|
6671
|
+
headers: this.getHeaders()
|
|
6549
6672
|
});
|
|
6550
6673
|
if (!response.ok) throw new Error(`Failed to leave spectator: ${response.statusText}`);
|
|
6551
6674
|
}
|
|
@@ -6559,27 +6682,30 @@ var GameAPI = class {
|
|
|
6559
6682
|
return data.spectators || [];
|
|
6560
6683
|
}
|
|
6561
6684
|
// --- Ranking/Leaderboard API ---
|
|
6562
|
-
|
|
6685
|
+
// 1.1.0 부터 활성화. 백엔드가 `app_id` + `game_type` 쿼리를 요구하므로 SDK 시그니처에 `gameType` 추가.
|
|
6686
|
+
async getLeaderboard(gameType, top = 100, season = "default") {
|
|
6563
6687
|
const id = this.appId || "";
|
|
6564
|
-
const
|
|
6565
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top
|
|
6688
|
+
const params = new URLSearchParams({ app_id: id, game_type: gameType, season, limit: String(top) });
|
|
6689
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?${params}`, {
|
|
6566
6690
|
headers: this.getHeaders()
|
|
6567
6691
|
});
|
|
6568
6692
|
if (!response.ok) throw new Error(`Failed to get leaderboard: ${response.statusText}`);
|
|
6569
6693
|
const data = await response.json();
|
|
6570
6694
|
return data.entries || [];
|
|
6571
6695
|
}
|
|
6572
|
-
async getPlayerStats(playerId) {
|
|
6696
|
+
async getPlayerStats(playerId, gameType, season = "default") {
|
|
6573
6697
|
const id = this.appId || "";
|
|
6574
|
-
const
|
|
6698
|
+
const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
|
|
6699
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats?${params}`, {
|
|
6575
6700
|
headers: this.getHeaders()
|
|
6576
6701
|
});
|
|
6577
6702
|
if (!response.ok) throw new Error(`Failed to get player stats: ${response.statusText}`);
|
|
6578
6703
|
return response.json();
|
|
6579
6704
|
}
|
|
6580
|
-
async getPlayerRank(playerId) {
|
|
6705
|
+
async getPlayerRank(playerId, gameType, season = "default") {
|
|
6581
6706
|
const id = this.appId || "";
|
|
6582
|
-
const
|
|
6707
|
+
const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
|
|
6708
|
+
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank?${params}`, {
|
|
6583
6709
|
headers: this.getHeaders()
|
|
6584
6710
|
});
|
|
6585
6711
|
if (!response.ok) throw new Error(`Failed to get player rank: ${response.statusText}`);
|
|
@@ -6615,11 +6741,16 @@ var GameAPI = class {
|
|
|
6615
6741
|
if (!response.ok) throw new Error(`Failed to set mute: ${response.statusText}`);
|
|
6616
6742
|
}
|
|
6617
6743
|
// --- Replay API ---
|
|
6744
|
+
// 1.1.0 부터 활성화. 백엔드에서 `REPLAY_STORAGE_PATH` 환경변수가 설정된 경우에만 동작.
|
|
6745
|
+
// 미설정 시 404 응답 → SDK 가 명시적 에러를 throw.
|
|
6618
6746
|
async listReplays(roomId) {
|
|
6619
6747
|
const id = this.appId || "";
|
|
6620
6748
|
let url = `${this.gameServerUrl}/v1/game/${id}/replays`;
|
|
6621
6749
|
if (roomId) url += `?room_id=${roomId}`;
|
|
6622
6750
|
const response = await fetch(url, { headers: this.getHeaders() });
|
|
6751
|
+
if (response.status === 404) {
|
|
6752
|
+
throw new Error("cb.game.listReplays: replay storage is not configured on this server (REPLAY_STORAGE_PATH unset).");
|
|
6753
|
+
}
|
|
6623
6754
|
if (!response.ok) throw new Error(`Failed to list replays: ${response.statusText}`);
|
|
6624
6755
|
const data = await response.json();
|
|
6625
6756
|
return data.replays || [];
|
|
@@ -6629,6 +6760,9 @@ var GameAPI = class {
|
|
|
6629
6760
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}`, {
|
|
6630
6761
|
headers: this.getHeaders()
|
|
6631
6762
|
});
|
|
6763
|
+
if (response.status === 404) {
|
|
6764
|
+
throw new Error("cb.game.getReplay: replay not found or replay storage unconfigured.");
|
|
6765
|
+
}
|
|
6632
6766
|
if (!response.ok) throw new Error(`Failed to get replay: ${response.statusText}`);
|
|
6633
6767
|
return response.json();
|
|
6634
6768
|
}
|
|
@@ -6637,6 +6771,9 @@ var GameAPI = class {
|
|
|
6637
6771
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/download`, {
|
|
6638
6772
|
headers: this.getHeaders()
|
|
6639
6773
|
});
|
|
6774
|
+
if (response.status === 404) {
|
|
6775
|
+
throw new Error("cb.game.downloadReplay: replay not found or replay storage unconfigured.");
|
|
6776
|
+
}
|
|
6640
6777
|
if (!response.ok) throw new Error(`Failed to download replay: ${response.statusText}`);
|
|
6641
6778
|
return response.arrayBuffer();
|
|
6642
6779
|
}
|
|
@@ -6645,6 +6782,9 @@ var GameAPI = class {
|
|
|
6645
6782
|
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/highlights`, {
|
|
6646
6783
|
headers: this.getHeaders()
|
|
6647
6784
|
});
|
|
6785
|
+
if (response.status === 404) {
|
|
6786
|
+
throw new Error("cb.game.getReplayHighlights: replay not found or replay storage unconfigured.");
|
|
6787
|
+
}
|
|
6648
6788
|
if (!response.ok) throw new Error(`Failed to get highlights: ${response.statusText}`);
|
|
6649
6789
|
const data = await response.json();
|
|
6650
6790
|
return data.highlights || [];
|
|
@@ -6675,15 +6815,18 @@ var AdsAPI = class {
|
|
|
6675
6815
|
return this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
6676
6816
|
}
|
|
6677
6817
|
/**
|
|
6678
|
-
* AdSense 연결 상태 확인
|
|
6818
|
+
* AdSense / AdMob 연결 상태 확인 (중첩 구조)
|
|
6679
6819
|
*
|
|
6680
|
-
* @returns
|
|
6820
|
+
* @returns `{ adsense, admob }` 각각 연결 상태·이메일·계정 ID
|
|
6681
6821
|
*
|
|
6682
6822
|
* @example
|
|
6683
6823
|
* ```typescript
|
|
6684
6824
|
* const status = await cb.ads.getConnectionStatus()
|
|
6685
|
-
* if (status.is_connected) {
|
|
6686
|
-
* console.log('연결됨:', status.email)
|
|
6825
|
+
* if (status.adsense.is_connected) {
|
|
6826
|
+
* console.log('AdSense 연결됨:', status.adsense.email, status.adsense.account_id)
|
|
6827
|
+
* }
|
|
6828
|
+
* if (status.admob.is_connected) {
|
|
6829
|
+
* console.log('AdMob 연결됨:', status.admob.account_id, status.admob.publisher_id)
|
|
6687
6830
|
* }
|
|
6688
6831
|
* ```
|
|
6689
6832
|
*/
|