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/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 ?? "localStorage";
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 simpleHash(str) {
376
+ function credentialToStorageKeyHash(credential) {
351
377
  let hash = 0;
352
- for (let i = 0; i < str.length; i++) {
353
- const char = str.charCodeAt(i);
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
- if (typeof sessionStorage === "undefined") return;
510
- sessionStorage.removeItem(this.getGuestMemberTokenKey());
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 = simpleHash(credential);
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
- if (typeof sessionStorage === "undefined") return null;
591
- const stored = sessionStorage.getItem(this.getGuestMemberTokenKey());
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
- if (typeof sessionStorage === "undefined") return;
601
- sessionStorage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
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
- const prefix = this.getPublicPrefix();
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
- `${prefix}/storages/files/${storageId}/items/${fileId}/move`,
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
- const prefix = this.getPublicPrefix();
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
- `${prefix}/storages/files/${storageId}/items/${fileId}/rename`,
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
- const response = await this.http.get("/v1/ice-servers");
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}/webrtc/stats`);
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}/webrtc/rooms`);
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 prepareResult = await client.payment.prepare({
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
- * const tossPayments = TossPayments(prepareResult.toss_client_key)
4253
- * await tossPayments.requestPayment('카드', {
4254
- * amount: prepareResult.amount,
4255
- * orderId: prepareResult.order_id,
4256
- * orderName: prepareResult.order_name,
4257
- * customerKey: prepareResult.customer_key,
4258
- * successUrl: prepareResult.success_url,
4259
- * failUrl: prepareResult.fail_url
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
- * // const result = await client.payment.prepare({ amount: 1000, order_name: 'Product' })
4264
- * // if (result.payment_provider === 'stripe') {
4265
- * // const stripe = await loadStripe(result.stripe_publishable_key)
4266
- * // const elements = stripe.elements({ clientSecret: result.stripe_client_secret })
4267
- * // // Mount PaymentElement, then:
4268
- * // // stripe.confirmPayment({ elements, redirect: 'if_required' })
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(appId, config = {}) {
6082
- const id = appId || this.appId || "";
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(roomId) {
6107
- const id = this.appId || "";
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
- async joinParty(partyId, playerId) {
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 response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/join`, {
6527
- method: "POST",
6528
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6529
- body: JSON.stringify({ player_id: playerId })
6530
- });
6531
- if (!response.ok) throw new Error(`Failed to join party: ${response.statusText}`);
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({ player_id: playerId, message })
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}/spectate`, {
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}/spectate/stop`, {
6584
- method: "POST",
6585
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
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
- async getLeaderboard(top) {
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 limit = top || 100;
6603
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?limit=${limit}`, {
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 response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats`, {
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 response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank`, {
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 연결 상태, 이메일, AdSense 계정 ID
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
  */