connectbase-client 1.12.0 → 3.0.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.mjs CHANGED
@@ -1613,41 +1613,6 @@ var DatabaseAPI = class {
1613
1613
  }
1614
1614
  };
1615
1615
  }
1616
- /**
1617
- * 프레즌스 상태 설정
1618
- */
1619
- setPresence(status, device, metadata) {
1620
- if (this.realtimeState !== "connected") return;
1621
- this.sendRealtimeMessage({
1622
- type: "presence_set",
1623
- request_id: this.generateRequestId(),
1624
- status,
1625
- device,
1626
- metadata
1627
- });
1628
- }
1629
- /**
1630
- * 프레즌스 구독 (다른 사용자의 온라인 상태 감시)
1631
- */
1632
- subscribePresence(userIds, onPresence) {
1633
- if (this.realtimeState !== "connected") return;
1634
- this.realtimeHandlers.set("__presence__", {
1635
- onSnapshot: (docs) => {
1636
- const states = {};
1637
- for (const doc of docs) {
1638
- if (doc.data) {
1639
- states[doc.id] = doc.data;
1640
- }
1641
- }
1642
- onPresence(states);
1643
- }
1644
- });
1645
- this.sendRealtimeMessage({
1646
- type: "presence_subscribe",
1647
- request_id: this.generateRequestId(),
1648
- user_ids: userIds
1649
- });
1650
- }
1651
1616
  /**
1652
1617
  * 실시간 연결 상태 확인
1653
1618
  */
@@ -1689,7 +1654,7 @@ var DatabaseAPI = class {
1689
1654
  this.setRealtimeState("connecting");
1690
1655
  const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
1691
1656
  const wsUrl = baseUrl.replace(/^http/, "ws");
1692
- const url = `${wsUrl}/v1/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
1657
+ const url = `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
1693
1658
  return new Promise((resolve, reject) => {
1694
1659
  try {
1695
1660
  this.realtimeWs = new WebSocket(url);
@@ -6609,495 +6574,6 @@ var GameAPI = class {
6609
6574
  async deleteRoom(_roomId) {
6610
6575
  throw new Error("cb.game.deleteRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
6611
6576
  }
6612
- // ==================== Matchmaking ====================
6613
- /**
6614
- * 매치메이킹 큐에 참가
6615
- */
6616
- async joinQueue(req) {
6617
- const id = this.appId || "";
6618
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/matchmaking/queue`, {
6619
- method: "POST",
6620
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6621
- body: JSON.stringify({
6622
- game_type: req.gameType,
6623
- player_id: req.playerId,
6624
- rating: req.rating,
6625
- region: req.region,
6626
- mode: req.mode,
6627
- party_members: req.partyMembers,
6628
- metadata: req.metadata
6629
- })
6630
- });
6631
- if (!response.ok) {
6632
- const err = await response.json().catch(() => ({}));
6633
- throw new Error(err.error || `Failed to join queue: ${response.statusText}`);
6634
- }
6635
- const data = await response.json();
6636
- return {
6637
- ticketId: data.ticket_id,
6638
- gameType: data.game_type,
6639
- playerId: data.player_id,
6640
- status: data.status,
6641
- createdAt: data.created_at
6642
- };
6643
- }
6644
- /**
6645
- * 매치메이킹 큐에서 탈퇴
6646
- */
6647
- async leaveQueue(ticketId) {
6648
- const id = this.appId || "";
6649
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/matchmaking/queue`, {
6650
- method: "DELETE",
6651
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6652
- body: JSON.stringify({ ticket_id: ticketId })
6653
- });
6654
- if (!response.ok) {
6655
- const err = await response.json().catch(() => ({}));
6656
- throw new Error(err.error || `Failed to leave queue: ${response.statusText}`);
6657
- }
6658
- }
6659
- /**
6660
- * 매치메이킹 상태 조회
6661
- */
6662
- async getMatchStatus(params) {
6663
- const id = this.appId || "";
6664
- const searchParams = new URLSearchParams();
6665
- if (params.ticketId) searchParams.set("ticket_id", params.ticketId);
6666
- if (params.playerId) searchParams.set("player_id", params.playerId);
6667
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/matchmaking/status?${searchParams}`, {
6668
- headers: this.getHeaders()
6669
- });
6670
- if (!response.ok) {
6671
- const err = await response.json().catch(() => ({}));
6672
- throw new Error(err.error || `Failed to get match status: ${response.statusText}`);
6673
- }
6674
- const data = await response.json();
6675
- return {
6676
- ticketId: data.ticket_id,
6677
- gameType: data.game_type,
6678
- playerId: "",
6679
- status: data.status,
6680
- createdAt: "",
6681
- waitTime: data.wait_time,
6682
- matchId: data.match_id,
6683
- roomId: data.room_id
6684
- };
6685
- }
6686
- // ==================== Lobbies ====================
6687
- /**
6688
- * 로비 목록 조회
6689
- */
6690
- async listLobbies() {
6691
- const id = this.appId || "";
6692
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies`, {
6693
- headers: this.getHeaders()
6694
- });
6695
- if (!response.ok) {
6696
- throw new Error(`Failed to list lobbies: ${response.statusText}`);
6697
- }
6698
- const data = await response.json();
6699
- return (data.lobbies || []).map((l) => ({
6700
- id: l.id,
6701
- name: l.name,
6702
- hostId: l.host_id,
6703
- gameType: l.game_type,
6704
- playerCount: l.player_count,
6705
- maxPlayers: l.max_players,
6706
- hasPassword: l.has_password,
6707
- visibility: l.visibility,
6708
- region: l.region,
6709
- settings: l.settings,
6710
- tags: l.tags,
6711
- status: l.status,
6712
- createdAt: l.created_at
6713
- }));
6714
- }
6715
- /**
6716
- * 로비 생성
6717
- */
6718
- async createLobby(req) {
6719
- const id = this.appId || "";
6720
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies`, {
6721
- method: "POST",
6722
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6723
- body: JSON.stringify({
6724
- player_id: req.playerId,
6725
- display_name: req.displayName,
6726
- name: req.name,
6727
- game_type: req.gameType,
6728
- password: req.password,
6729
- max_players: req.maxPlayers,
6730
- visibility: req.visibility,
6731
- region: req.region,
6732
- settings: req.settings,
6733
- tags: req.tags
6734
- })
6735
- });
6736
- if (!response.ok) {
6737
- const err = await response.json().catch(() => ({}));
6738
- throw new Error(err.error || `Failed to create lobby: ${response.statusText}`);
6739
- }
6740
- const data = await response.json();
6741
- return {
6742
- id: data.id,
6743
- name: data.name,
6744
- hostId: data.host_id,
6745
- gameType: data.game_type,
6746
- playerCount: 1,
6747
- maxPlayers: data.max_players,
6748
- hasPassword: !!req.password,
6749
- visibility: data.visibility,
6750
- region: data.region,
6751
- settings: data.settings,
6752
- tags: data.tags,
6753
- status: data.status,
6754
- createdAt: data.created_at
6755
- };
6756
- }
6757
- /**
6758
- * 로비 상세 조회
6759
- */
6760
- async getLobby(lobbyId) {
6761
- const id = this.appId || "";
6762
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}`, {
6763
- headers: this.getHeaders()
6764
- });
6765
- if (!response.ok) {
6766
- throw new Error(`Failed to get lobby: ${response.statusText}`);
6767
- }
6768
- const data = await response.json();
6769
- return {
6770
- id: data.id,
6771
- name: data.name,
6772
- hostId: data.host_id,
6773
- gameType: data.game_type,
6774
- playerCount: data.player_count,
6775
- maxPlayers: data.max_players,
6776
- hasPassword: data.has_password,
6777
- visibility: data.visibility,
6778
- region: data.region,
6779
- settings: data.settings,
6780
- tags: data.tags,
6781
- status: data.status,
6782
- roomId: data.room_id,
6783
- members: (data.members || []).map((m) => ({
6784
- playerId: m.player_id,
6785
- displayName: m.display_name,
6786
- role: m.role,
6787
- team: m.team,
6788
- ready: m.ready,
6789
- slot: m.slot,
6790
- joinedAt: m.joined_at
6791
- })),
6792
- createdAt: data.created_at
6793
- };
6794
- }
6795
- /**
6796
- * 로비 참가
6797
- */
6798
- async joinLobby(lobbyId, playerId, displayName, password) {
6799
- const id = this.appId || "";
6800
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/join`, {
6801
- method: "POST",
6802
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6803
- body: JSON.stringify({ player_id: playerId, display_name: displayName, password })
6804
- });
6805
- if (!response.ok) {
6806
- const err = await response.json().catch(() => ({}));
6807
- throw new Error(err.error || `Failed to join lobby: ${response.statusText}`);
6808
- }
6809
- const data = await response.json();
6810
- return { lobbyId: data.lobby_id };
6811
- }
6812
- /**
6813
- * 로비 퇴장
6814
- */
6815
- async leaveLobby(lobbyId, playerId) {
6816
- const id = this.appId || "";
6817
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/leave`, {
6818
- method: "POST",
6819
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6820
- body: JSON.stringify({ player_id: playerId })
6821
- });
6822
- if (!response.ok) {
6823
- const err = await response.json().catch(() => ({}));
6824
- throw new Error(err.error || `Failed to leave lobby: ${response.statusText}`);
6825
- }
6826
- }
6827
- /**
6828
- * 레디 상태 토글
6829
- */
6830
- async toggleReady(lobbyId, playerId, ready) {
6831
- const id = this.appId || "";
6832
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/ready`, {
6833
- method: "POST",
6834
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6835
- body: JSON.stringify({ player_id: playerId, ready })
6836
- });
6837
- if (!response.ok) {
6838
- const err = await response.json().catch(() => ({}));
6839
- throw new Error(err.error || `Failed to toggle ready: ${response.statusText}`);
6840
- }
6841
- }
6842
- /**
6843
- * 게임 시작 (호스트만)
6844
- */
6845
- async startGame(lobbyId, hostId) {
6846
- const id = this.appId || "";
6847
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/start`, {
6848
- method: "POST",
6849
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6850
- body: JSON.stringify({ host_id: hostId })
6851
- });
6852
- if (!response.ok) {
6853
- const err = await response.json().catch(() => ({}));
6854
- throw new Error(err.error || `Failed to start game: ${response.statusText}`);
6855
- }
6856
- const data = await response.json();
6857
- return { roomId: data.room_id };
6858
- }
6859
- /**
6860
- * 플레이어 추방 (호스트만)
6861
- */
6862
- async kickPlayer(lobbyId, hostId, targetPlayerId) {
6863
- const id = this.appId || "";
6864
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/kick/${targetPlayerId}`, {
6865
- method: "POST",
6866
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6867
- body: JSON.stringify({ host_id: hostId })
6868
- });
6869
- if (!response.ok) {
6870
- const err = await response.json().catch(() => ({}));
6871
- throw new Error(err.error || `Failed to kick player: ${response.statusText}`);
6872
- }
6873
- }
6874
- /**
6875
- * 로비 설정 업데이트 (호스트만)
6876
- */
6877
- async updateLobby(lobbyId, req) {
6878
- const id = this.appId || "";
6879
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}`, {
6880
- method: "PATCH",
6881
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6882
- body: JSON.stringify({
6883
- host_id: req.hostId,
6884
- name: req.name,
6885
- max_players: req.maxPlayers,
6886
- visibility: req.visibility,
6887
- password: req.password,
6888
- settings: req.settings,
6889
- tags: req.tags
6890
- })
6891
- });
6892
- if (!response.ok) {
6893
- const err = await response.json().catch(() => ({}));
6894
- throw new Error(err.error || `Failed to update lobby: ${response.statusText}`);
6895
- }
6896
- const data = await response.json();
6897
- return {
6898
- id: data.id,
6899
- name: data.name,
6900
- hostId: "",
6901
- gameType: "",
6902
- playerCount: 0,
6903
- maxPlayers: data.max_players,
6904
- hasPassword: false,
6905
- visibility: data.visibility,
6906
- region: "",
6907
- settings: data.settings,
6908
- tags: data.tags,
6909
- status: "",
6910
- createdAt: ""
6911
- };
6912
- }
6913
- /**
6914
- * 로비 채팅 전송
6915
- */
6916
- async sendLobbyChat(lobbyId, playerId, message) {
6917
- const id = this.appId || "";
6918
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/chat`, {
6919
- method: "POST",
6920
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6921
- body: JSON.stringify({ player_id: playerId, message })
6922
- });
6923
- if (!response.ok) {
6924
- const err = await response.json().catch(() => ({}));
6925
- throw new Error(err.error || `Failed to send chat: ${response.statusText}`);
6926
- }
6927
- }
6928
- /**
6929
- * 플레이어 초대
6930
- */
6931
- async invitePlayer(lobbyId, inviterId, inviteeId) {
6932
- const id = this.appId || "";
6933
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/invite`, {
6934
- method: "POST",
6935
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6936
- body: JSON.stringify({ inviter_id: inviterId, invitee_id: inviteeId })
6937
- });
6938
- if (!response.ok) {
6939
- const err = await response.json().catch(() => ({}));
6940
- throw new Error(err.error || `Failed to invite player: ${response.statusText}`);
6941
- }
6942
- const data = await response.json();
6943
- return {
6944
- inviteId: data.invite_id,
6945
- lobbyId: data.lobby_id,
6946
- lobbyName: data.lobby_name,
6947
- inviterId: data.inviter_id,
6948
- inviteeId: data.invitee_id,
6949
- status: "pending",
6950
- createdAt: "",
6951
- expiresAt: data.expires_at
6952
- };
6953
- }
6954
- /**
6955
- * 초대 수락
6956
- */
6957
- async acceptInvite(inviteId, playerId, displayName) {
6958
- const id = this.appId || "";
6959
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/invites/${inviteId}/accept`, {
6960
- method: "POST",
6961
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6962
- body: JSON.stringify({ player_id: playerId, display_name: displayName })
6963
- });
6964
- if (!response.ok) {
6965
- const err = await response.json().catch(() => ({}));
6966
- throw new Error(err.error || `Failed to accept invite: ${response.statusText}`);
6967
- }
6968
- const data = await response.json();
6969
- return { lobbyId: data.lobby_id };
6970
- }
6971
- /**
6972
- * 초대 거절
6973
- */
6974
- async declineInvite(inviteId, playerId) {
6975
- const id = this.appId || "";
6976
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/invites/${inviteId}/decline`, {
6977
- method: "POST",
6978
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6979
- body: JSON.stringify({ player_id: playerId })
6980
- });
6981
- if (!response.ok) {
6982
- const err = await response.json().catch(() => ({}));
6983
- throw new Error(err.error || `Failed to decline invite: ${response.statusText}`);
6984
- }
6985
- }
6986
- /**
6987
- * 플레이어의 받은 초대 목록
6988
- */
6989
- async getPlayerInvites(playerId) {
6990
- const id = this.appId || "";
6991
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/player/${playerId}/invites`, {
6992
- headers: this.getHeaders()
6993
- });
6994
- if (!response.ok) {
6995
- throw new Error(`Failed to get invites: ${response.statusText}`);
6996
- }
6997
- const data = await response.json();
6998
- return (data.invites || []).map((inv) => ({
6999
- inviteId: inv.invite_id,
7000
- lobbyId: inv.lobby_id,
7001
- lobbyName: inv.lobby_name,
7002
- inviterId: inv.inviter_id,
7003
- inviteeId: "",
7004
- status: inv.status,
7005
- createdAt: inv.created_at,
7006
- expiresAt: inv.expires_at
7007
- }));
7008
- }
7009
- // --- Party API ---
7010
- // 1.1.0 부터 `createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화.
7011
- // 1.3.0 부터 `acceptPartyInvite/declinePartyInvite` 추가. 직접 join 엔드포인트는 없음 — `joinParty` 는 throw.
7012
- async createParty(playerId, maxSize) {
7013
- const id = this.appId || "";
7014
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties`, {
7015
- method: "POST",
7016
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
7017
- body: JSON.stringify({ player_id: playerId, max_size: maxSize || 4 })
7018
- });
7019
- if (!response.ok) throw new Error(`Failed to create party: ${response.statusText}`);
7020
- return response.json();
7021
- }
7022
- /**
7023
- * @deprecated 백엔드에서 직접 join 엔드포인트 미제공 — `inviteToParty` → `acceptPartyInvite` 플로우 사용
7024
- */
7025
- async joinParty(_partyId, _playerId) {
7026
- throw new Error("cb.game.joinParty is not supported \u2014 use inviteToParty + acceptPartyInvite flow.");
7027
- }
7028
- /**
7029
- * 파티 초대 수락
7030
- *
7031
- * `inviteToParty` 로 생성된 초대 ID 를 수락하여 파티에 합류한다.
7032
- * 로비 초대(`acceptInvite`) 와 다른 엔드포인트 (`/v1/game/:appID/invites/:inviteID/accept`).
7033
- */
7034
- async acceptPartyInvite(inviteId, playerId, displayName) {
7035
- const id = this.appId || "";
7036
- const qs = new URLSearchParams({ player_id: playerId });
7037
- if (displayName) qs.set("display_name", displayName);
7038
- const response = await fetch(
7039
- `${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/accept?${qs.toString()}`,
7040
- { method: "POST", headers: this.getHeaders() }
7041
- );
7042
- if (!response.ok) {
7043
- const err = await response.json().catch(() => ({}));
7044
- throw new Error(err.error || `Failed to accept party invite: ${response.statusText}`);
7045
- }
7046
- return response.json();
7047
- }
7048
- /**
7049
- * 파티 초대 거절
7050
- *
7051
- * `inviteToParty` 로 생성된 초대 ID 를 거절한다.
7052
- */
7053
- async declinePartyInvite(inviteId, playerId) {
7054
- const id = this.appId || "";
7055
- const qs = new URLSearchParams({ player_id: playerId });
7056
- const response = await fetch(
7057
- `${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/decline?${qs.toString()}`,
7058
- { method: "POST", headers: this.getHeaders() }
7059
- );
7060
- if (!response.ok) {
7061
- const err = await response.json().catch(() => ({}));
7062
- throw new Error(err.error || `Failed to decline party invite: ${response.statusText}`);
7063
- }
7064
- }
7065
- async leaveParty(partyId, playerId) {
7066
- const id = this.appId || "";
7067
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/leave`, {
7068
- method: "POST",
7069
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
7070
- body: JSON.stringify({ player_id: playerId })
7071
- });
7072
- if (!response.ok) throw new Error(`Failed to leave party: ${response.statusText}`);
7073
- }
7074
- async kickFromParty(partyId, playerId) {
7075
- const id = this.appId || "";
7076
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/kick/${playerId}`, {
7077
- method: "POST",
7078
- headers: this.getHeaders()
7079
- });
7080
- if (!response.ok) throw new Error(`Failed to kick from party: ${response.statusText}`);
7081
- }
7082
- async inviteToParty(partyId, inviterId, inviteeId) {
7083
- const id = this.appId || "";
7084
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/invite/${inviteeId}`, {
7085
- method: "POST",
7086
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
7087
- body: JSON.stringify({ inviter_id: inviterId })
7088
- });
7089
- if (!response.ok) throw new Error(`Failed to invite to party: ${response.statusText}`);
7090
- return response.json();
7091
- }
7092
- async sendPartyChat(partyId, playerId, message) {
7093
- const id = this.appId || "";
7094
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/chat`, {
7095
- method: "POST",
7096
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
7097
- body: JSON.stringify({ sender_id: playerId, content: message })
7098
- });
7099
- if (!response.ok) throw new Error(`Failed to send party chat: ${response.statusText}`);
7100
- }
7101
6577
  // --- Spectator API ---
7102
6578
  // 1.1.0 부터 활성화. 백엔드 엔드포인트는 `/rooms/:roomID/spectators` 를 사용 (기존 `/spectate` 아님).
7103
6579
  async joinSpectator(roomId, playerId) {
@@ -7127,36 +6603,6 @@ var GameAPI = class {
7127
6603
  const data = await response.json();
7128
6604
  return data.spectators || [];
7129
6605
  }
7130
- // --- Ranking/Leaderboard API ---
7131
- // 1.1.0 부터 활성화. 백엔드가 `app_id` + `game_type` 쿼리를 요구하므로 SDK 시그니처에 `gameType` 추가.
7132
- async getLeaderboard(gameType, top = 100, season = "default") {
7133
- const id = this.appId || "";
7134
- const params = new URLSearchParams({ app_id: id, game_type: gameType, season, limit: String(top) });
7135
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?${params}`, {
7136
- headers: this.getHeaders()
7137
- });
7138
- if (!response.ok) throw new Error(`Failed to get leaderboard: ${response.statusText}`);
7139
- const data = await response.json();
7140
- return data.entries || [];
7141
- }
7142
- async getPlayerStats(playerId, gameType, season = "default") {
7143
- const id = this.appId || "";
7144
- const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
7145
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats?${params}`, {
7146
- headers: this.getHeaders()
7147
- });
7148
- if (!response.ok) throw new Error(`Failed to get player stats: ${response.statusText}`);
7149
- return response.json();
7150
- }
7151
- async getPlayerRank(playerId, gameType, season = "default") {
7152
- const id = this.appId || "";
7153
- const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
7154
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank?${params}`, {
7155
- headers: this.getHeaders()
7156
- });
7157
- if (!response.ok) throw new Error(`Failed to get player rank: ${response.statusText}`);
7158
- return response.json();
7159
- }
7160
6606
  // --- Voice API ---
7161
6607
  async joinVoiceChannel(roomId, playerId) {
7162
6608
  const id = this.appId || "";
@@ -7247,6 +6693,166 @@ var GameAPI = class {
7247
6693
  }
7248
6694
  return headers;
7249
6695
  }
6696
+ // ─────────────────────────────────────────────────────────────────────
6697
+ // Phase 3 — Matchqueue primitive (mechanism only)
6698
+ //
6699
+ // 매칭 알고리즘 (어떤 ticket 끼리 묶을지) 은 사용자 Lua 가 list → notify 흐름으로
6700
+ // 직접 구현. attributes 는 free-form JSON 으로 rating/region/team_size 등 자유롭게 첨부.
6701
+ // 매칭 완료 시 사용자가 cb.realtime.subscribe(`matchqueue.${appID}.${queueKey}`) 로 수신.
6702
+ //
6703
+ // 자세한 패턴: docs/game-server/RECIPES.md §10
6704
+ // ─────────────────────────────────────────────────────────────────────
6705
+ /**
6706
+ * matchqueue 에 ticket 등록.
6707
+ *
6708
+ * @example
6709
+ * await cb.game.enqueueMatch(appId, "ranked", userId, {
6710
+ * rating: 1500, region: "asia", team_size: 5,
6711
+ * }, 60)
6712
+ *
6713
+ * @constraints appId/queueKey/ticketId 는 [a-zA-Z0-9_-] 만 허용. ttlSec=0 이면 만료 없음.
6714
+ */
6715
+ async enqueueMatch(appId, queueKey, ticketId, attributes, ttlSec) {
6716
+ return this.http.post(
6717
+ `/v1/game/${appId}/matchqueue/${queueKey}/tickets`,
6718
+ { ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 }
6719
+ );
6720
+ }
6721
+ /**
6722
+ * matchqueue 의 모든 ticket 목록 반환. 사용자 Lua 가 매칭 후보 선별에 사용.
6723
+ *
6724
+ * @example const tickets = await cb.game.listMatchqueue(appId, "ranked")
6725
+ */
6726
+ async listMatchqueue(appId, queueKey) {
6727
+ return this.http.get(
6728
+ `/v1/game/${appId}/matchqueue/${queueKey}`
6729
+ );
6730
+ }
6731
+ /**
6732
+ * 매칭 큐에서 ticket 제거 (예: 사용자가 매칭 취소).
6733
+ */
6734
+ async cancelMatch(appId, queueKey, ticketId) {
6735
+ await this.http.delete(
6736
+ `/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`
6737
+ );
6738
+ }
6739
+ // ─────────────────────────────────────────────────────────────────────
6740
+ // Phase 4 — Leaderboard primitive (mechanism only)
6741
+ //
6742
+ // 시즌은 사용자가 leaderboardKey suffix 로 분리 (예: "ranks:2026q2"). ELO/티어 공식은
6743
+ // 사용자 Lua 가 incrScore 호출로 결과 기록 (RECIPES.md §6 참고).
6744
+ // ─────────────────────────────────────────────────────────────────────
6745
+ /**
6746
+ * leaderboard 점수 기록. mode="set" (기본, overwrite) 또는 "incr" (증감).
6747
+ *
6748
+ * @example
6749
+ * await cb.game.submitScore(appId, "global_elo", userId, 32, "incr")
6750
+ */
6751
+ async submitScore(appId, leaderboardKey, member, score, mode = "set") {
6752
+ return this.http.post(
6753
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/scores`,
6754
+ { member, score, mode }
6755
+ );
6756
+ }
6757
+ /**
6758
+ * 상위 n 명 조회 (기본 10).
6759
+ */
6760
+ async getTopScores(appId, leaderboardKey, n = 10) {
6761
+ return this.http.get(
6762
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`
6763
+ );
6764
+ }
6765
+ /**
6766
+ * 단일 member 의 rank + score.
6767
+ */
6768
+ async getMemberRank(appId, leaderboardKey, member) {
6769
+ return this.http.get(
6770
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
6771
+ );
6772
+ }
6773
+ /**
6774
+ * member 주변 (위 above 명 + 본인 + 아래 below 명) 조회.
6775
+ */
6776
+ async getRankAround(appId, leaderboardKey, member, above = 5, below = 5) {
6777
+ return this.http.get(
6778
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`
6779
+ );
6780
+ }
6781
+ /**
6782
+ * leaderboard 전체 reset (시즌 종료 시).
6783
+ */
6784
+ async resetLeaderboard(appId, leaderboardKey) {
6785
+ await this.http.delete(`/v1/game/${appId}/leaderboards/${leaderboardKey}`);
6786
+ }
6787
+ /**
6788
+ * 특정 member 만 제거 (계정 삭제 등).
6789
+ */
6790
+ async removeFromLeaderboard(appId, leaderboardKey, member) {
6791
+ await this.http.delete(
6792
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
6793
+ );
6794
+ }
6795
+ // ─────────────────────────────────────────────────────────────────────
6796
+ // Phase 5 — Lua 스크립트 관리 (메타데이터 + 버전 + hot reload)
6797
+ //
6798
+ // 활성화 시 NATS publish 로 모든 게임 서버 인스턴스가 VM pool 핫 리로드.
6799
+ // ─────────────────────────────────────────────────────────────────────
6800
+ /**
6801
+ * 새 스크립트 버전 업로드 (latest+1). idempotent — 동일 hash 면 새 버전 만들지 않음.
6802
+ *
6803
+ * @example
6804
+ * await cb.game.uploadScript(appId, "ranked_br", fs.readFileSync("./ranked.lua", "utf-8"))
6805
+ */
6806
+ async uploadScript(appId, name, code) {
6807
+ return this.http.post(
6808
+ `/v1/game/${appId}/scripts`,
6809
+ { name, code }
6810
+ );
6811
+ }
6812
+ /**
6813
+ * app 의 모든 스크립트 메타데이터 목록.
6814
+ */
6815
+ async listScripts(appId) {
6816
+ return this.http.get(`/v1/game/${appId}/scripts`);
6817
+ }
6818
+ /**
6819
+ * 단일 스크립트 메타 + active version code.
6820
+ */
6821
+ async getScript(appId, name) {
6822
+ return this.http.get(`/v1/game/${appId}/scripts/${name}`);
6823
+ }
6824
+ /**
6825
+ * 단일 스크립트의 모든 버전 이력.
6826
+ */
6827
+ async listScriptVersions(appId, name) {
6828
+ return this.http.get(
6829
+ `/v1/game/${appId}/scripts/${name}/versions`
6830
+ );
6831
+ }
6832
+ /**
6833
+ * 특정 버전 활성화 (hot reload 자동). version=0 또는 미지정 → latest 활성화.
6834
+ */
6835
+ async activateScript(appId, name, version) {
6836
+ return this.http.post(
6837
+ `/v1/game/${appId}/scripts/${name}/activate`,
6838
+ { version: version ?? 0 }
6839
+ );
6840
+ }
6841
+ /**
6842
+ * 직전 active 버전으로 롤백 (hot reload 자동).
6843
+ */
6844
+ async rollbackScript(appId, name) {
6845
+ return this.http.post(
6846
+ `/v1/game/${appId}/scripts/${name}/rollback`,
6847
+ {}
6848
+ );
6849
+ }
6850
+ /**
6851
+ * 스크립트 비활성화 (status=disabled). 코드/버전은 보존.
6852
+ */
6853
+ async disableScript(appId, name) {
6854
+ await this.http.delete(`/v1/game/${appId}/scripts/${name}`);
6855
+ }
7250
6856
  };
7251
6857
 
7252
6858
  // src/api/ads.ts