# 사용자 지정 순위표

**URL:** https://heroiclabs.com/docs/kr/nakama/guides/concepts/custom-leaderboards/
**Summary:** 이 가이드에서는 플레이어가 특정 그룹이나 친구를 대상으로 플레이를 통해 정적인 글로벌 순위표와는 반대로 관계를 유지할 수 있도록 사용자 지정 순위표를 실행하는 예시를 제공합니다.

---


# 친구 및 그룹 순위표

Nakama [순위표](../../../concepts/leaderboards/)를 통해 플레이어는 경쟁 활동에 참여할 수 있습니다. 게임이 진행됨에 따라, 플레이어가 순위를 높이기 어려워지기 때문에 순위표가 고정된 것처럼 느껴질 수 있습니다. 다음을 통해 문제를 해결할 수 있습니다:

- [버킷으로 구성된 순위표](../bucketed-leaderboards/)
- 그룹 또는 친구 순위표

순위표 목록 API를 사용하여 사용자 ID 목록을 전달하여 사용자 지정 순위표 보기를 생성할 수 있습니다.

다음의 코드 샘플은 그룹의 구성원 또는 사용자의 친구에 대한 순위표 레코드를 얻을 수 있는 방법에 대해서 설명합니다.

## 순위표 생성하기

매주 월요일 00:00에 재설정되는 순위표를 서버에 생성합니다.

{{< code type="server" >}}
```go
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // Create a weekly leaderboard
  id := "weekly_leaderboard"
  authoritative := false
  sortOrder := "desc"
  operator := "incr"
  resetSchedule := "0 0 * * 1"
  metadata := make(map[string]interface{})
  ranked := false

  if err := nk.LeaderboardCreate(ctx, id, authoritative, sortOrder, operator, resetSchedule, metadata, ranked); err != nil {
    logger.Error("error creating leaderboard")
    return err
  }
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  const id = 'weekly_leaderboard';
  const authoritative = false;
  const sortOrder = nkruntime.SortOrder.DESCENDING;
  const operator = nkruntime.Operator.INCREMENTAL;
  const resetSchedule = '0 0 * * 1';
  const metadata = {};
  const ranked = false;

  nk.leaderboardCreate(id, authoritative, sortOrder, operator, resetSchedule, metadata, ranked);
}
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

## 순위표에 대한 사용자 지정 보기

RPC에 대한 페이로드 구조를 정의합니다:

{{< code type="server" >}}
```go
type leaderboardRecord struct {
  Username string `json:"username"`
  UserId   string `json:"userId"`
  Score    int    `json:"score"`
  Rank     int    `json:"rank"`
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
interface LeaderboardRecord {
  username: string,
  userId: string,
  score: number,
  rank: number
}
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

일련의 사용자 ID를 사용하여 일련의 레코드와 함께 사용자 점수를 기반으로 상대 순위값을 반환하는 도우미 기능을 생성합니다.

{{< code type="server">}}
```go
func getLeaderboardForUsers(leaderboardId string, userIds []string, ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule) ([]leaderboardRecord, error) {
  // Get all leaderboard records for user Ids
  _, records, _, _, err := nk.LeaderboardRecordsList(ctx, leaderboardId, userIds, 0, "", 0)
  if err != nil {
    return nil, err
  }

  // Create result slice and add a rank value
  results := []leaderboardRecord{}
  for i, record := range records {
    r := leaderboardRecord{
      Username: record.Username.Value,
      UserId:   record.OwnerId,
      Score:    int(record.Score),
      Rank:     len(records) - i,
    }
    results = append(results, r)
  }

  return results, nil
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
const getLeaderboardForUsers = function (leaderboardId: string, userIds: string[], logger: nkruntime.Logger, nk: nkruntime.Nakama): LeaderboardRecord[] {
  const recordsList = nk.leaderboardRecordsList(leaderboardId, userIds);
  const results: LeaderboardRecord[] = [];

  recordsList.records.forEach(function (leaderboardRecord, i) {
    results.push({
      username: leaderboardRecord.username,
      userId: leaderboardRecord.ownerId,
      score: leaderboardRecord.score,
      rank: recordsList.records.length - i
    });
  });

  return results;
}
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

## 그룹 순위표 보기

일반적인 기능을 통해 사용자 목록에 대한 순위표를 얻을 수 있기 때문에 그룹에 대한 순위표를 얻을 수 있는 기능을 생성합니다.

RPC에 대한 페이로드 구조를 정의합니다:

{{< code type="server">}}
```go
type groupLeaderboardRecordsPayload struct {
  GroupId       string `json:"groupId"`
  LeaderboardId string `json:"leaderboardId"`
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
interface groupLeaderboardRecordsPayload {
  groupId: string,
  leaderboardId: string
}
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

그룹에서 구성원의 목록과 순위표를 얻을 수 있는 기능을 생성합니다:

{{< code type="server">}}
```go
func getGroupLeaderboardRecords(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  // Unmarshal the payload
  data := &groupLeaderboardRecordsPayload{}
  if err := json.Unmarshal([]byte(payload), data); err != nil {
    logger.Error("error unmarshaling payload")
    return "", err
  }

  // Get leaderboard
  leaderboards, err := nk.LeaderboardsGetId(ctx, []string{data.LeaderboardId})
  if err != nil {
    logger.Error("error getting leaderboards")
    return "", err
  }

  if len(leaderboards) == 0 {
    errorMessage := fmt.Sprintf("error finding leaderboard: %s", data.LeaderboardId)
    logger.Error(errorMessage)
    return "", errors.New(errorMessage)
  }

  // Get group members
  members, _, err := nk.GroupUsersList(ctx, data.GroupId, 100, nil, "")
  if err != nil {
    logger.Error("error getting group members")
    return "", err
  }

  // Get a slice of memberIds
  memberIds := []string{}
  for _, member := range members {
    memberIds = append(memberIds, member.User.Id)
  }

  // Get all leaderboard records for users
  results, err := getLeaderboardForUsers(leaderboards[0].Id, memberIds, ctx, logger, nk)
  if err != nil {
    logger.Error("error getting leaderboard records")
    return "", err
  }

  // Return the leaderboard records to the user
  bytes, err := json.Marshal(results)
  if err != nil {
    logger.Error("error marshaling result")
    return "", err
  }

  return string(bytes), nil
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
const getGroupLeaderboardRecords: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string | void {
  // Parse the payload
  const data = <groupLeaderboardRecordsPayload> JSON.parse(payload);
  
  if (!data) {
    throw new Error('Error parsing payload');
  }
  
  // Get leaderboard
  const leaderboards = nk.leaderboardsGetId([data.leaderboardId]);
  
  if (leaderboards.length == 0) {
    throw new Error(`Error finding leaderboard: ${data.leaderboardId}`);
  }
  
  // Get group members
  const members = nk.groupUsersList(data.groupId, 100);
  
  // Get a slice of memberIds
  const memberIds: string[] = [];
  members.groupUsers.forEach(function (groupUser) {
    memberIds.push(groupUser.user.userId);
  });
  
  // Get all leaderboard records for users
  const results = getLeaderboardForUsers(leaderboards[0].id, memberIds, logger, nk);
  
  // Return the leaderboard records to the user
  return JSON.stringify(results);
};
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

기능을 등록하고 클라이언트에서 호출할 수 있는 원격 프로시저로 노출시킵니다:

{{< code type="server">}}
```go
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // ...

  // Register RPC to retrieve a custom view over a leaderboard based on group members
  if err := initializer.RegisterRpc("getGroupLeaderboardRecords", getGroupLeaderboardRecords); err != nil {
    logger.Error(`error registering "getGroupLeaderboardRecords" rpc`)
    return err
  }

  return nil
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  // ... 

  // Register RPC to retrieve a custom view over a leaderboard based on group members
  initializer.registerRpc('getGroupLeaderboardRecords', getGroupLeaderboardRecords);
}
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

## 친구 순위표 보기

이와 유사하게, 사용자의 친구에 대한 순위표를 얻을 수 있는 기능을 생성합니다.

RPC에 대한 페이로드 구조를 정의합니다:

{{< code type="server">}}
```go
type friendsLeaderboardRecordsPayload struct {
  LeaderboardId string `json:"leaderboardId"`
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
interface friendsLeaderboardReocrdsPayload {
  leaderboardId: string
}
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

사용자의 친구 목록과 순위표를 얻을 수 있는 기능을 생성합니다:

{{< code type="server">}}
```go
func getFriendLeaderboardRecords(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  // Unmarshal the payload
  data := &friendsLeaderboardRecordsPayload{}
  if err := json.Unmarshal([]byte(payload), data); err != nil {
    logger.Error("error unmarshaling payload")
    return "", err
  }

  // Get leaderboard
  leaderboards, err := nk.LeaderboardsGetId(ctx, []string{data.LeaderboardId})
  if err != nil {
    logger.Error("error getting leaderboards")
    return "", err
  }

  if len(leaderboards) == 0 {
    errorMessage := fmt.Sprintf("error finding leaderboard: %s", data.LeaderboardId)
    logger.Error(errorMessage)
    return "", errors.New(errorMessage)
  }

  // Get user id from context
  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    errorMessage := fmt.Sprintf("error getting user id from context")
    logger.Error(errorMessage)
    return "", errors.New(errorMessage)
  }

  // Get friends (where state is 0 - mutual friends)
  state := 0
  friends, _, err := nk.FriendsList(ctx, userId, 100, &state, "")
  if err != nil {
    logger.Error("error getting friends")
    return "", err
  }

  // Get a slice of memberIds
  friendIds := []string{}
  for _, member := range friends {
    friendIds = append(friendIds, member.User.Id)
  }

  // Get all leaderboard records for users
  results, err := getLeaderboardForUsers(leaderboards[0].Id, friendIds, ctx, logger, nk)
  if err != nil {
    logger.Error("error getting leaderboard records")
    return "", err
  }

  // Return the leaderboard records to the user
  bytes, err := json.Marshal(results)
  if err != nil {
    logger.Error("error marshaling result")
    return "", err
  }

  return string(bytes), nil
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
const getFriendLeaderboardRecords: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string | void {
  // Parse the payload
  const data = <friendsLeaderboardReocrdsPayload> JSON.parse(payload);

  if (!data) {
    throw new Error('Error parsing payload');
  }

  // Get leaderboard
  const leaderboards = nk.leaderboardsGetId([data.leaderboardId]);

  if (leaderboards.length == 0) {
    throw new Error(`Error finding leaderboard: ${data.leaderboardId}`);
  }

  // Get friends (where state is 0 - mutual friends)
  const friendsList = nk.friendsList(ctx.userId, 100, 0);

  // Get a slice of friendIds
  const friendIds: string[] = [];
  friendsList.friends.forEach(function (friend) {
    friendIds.push(friend.user.userId);
  });

  // Get all leaderboard records for users
  const results = getLeaderboardForUsers(leaderboards[0].id, friendIds, logger, nk);

  // Return the leaderboard records to the user
  return JSON.stringify(results);
};
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}

기능을 등록하고 클라이언트에서 호출할 수 있는 원격 프로시저로 노출시킵니다:

{{< code type="server">}}
```go
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // ...

  // Register RPC to retrieve a custom view over a leaderboard based on friends
  if err := initializer.RegisterRpc("getFriendLeaderboardRecords", getFriendLeaderboardRecords); err != nil {
    logger.Error(`error registering "getFriendLeaderboardRecords" rpc`)
    return err
  }

  return nil
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  // ...

  // Register RPC to retrieve a custom view over a leaderboard based on friends
  initializer.registerRpc('getFriendLeaderboardRecords', getFriendLeaderboardRecords);
}
```
{{< / code >}}

{{< missing type="server" lang="lua" / >}}
