> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Economy v2의 드롭 테이블 및 무작위 루트

> Title Data, Azure Functions, ExecuteInventoryOperations를 사용해 PlayFab Economy v2에서 원자적으로 아이템을 부여하는 가중 무작위 루트 드롭을 구축합니다.

<Info>
  Economy v2는 이제 정식 출시(GA)되었습니다. 지원 및 피드백은 [PlayFab 포럼](https://community.playfab.com)을 참조하세요.
</Info>

Economy v1(레거시)에는 Game Manager에서 직접 가중 무작위 아이템 분포를 정의할 수 있는 기본 제공 [드롭 테이블](/services/playfab/economy-monetization/economy/tutorials/drop-tables) 기능이 있었습니다. Economy v2에는 드롭 테이블이 기본 기능으로 포함되어 있지 않지만, [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview)와 [ExecuteInventoryOperations](https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/execute-inventory-operations) API를 결합해 동일한 동작을(더 유연하게) 구현할 수 있습니다.

이 튜토리얼에서는 다음을 수행하는 완전한 무작위 루트 시스템을 구축하는 방법을 보여줍니다.

* [Title Data](/services/playfab/live-service-management/game-configuration/titledata)에서 가중 아이템 풀로 루트 테이블을 정의합니다.
* Azure Function을 사용해 루트 테이블에서 무작위 아이템을 롤합니다.
* 단일 호출로 컨테이너 토큰을 원자적으로 차감하고 무작위로 선택된 아이템을 부여합니다.

## 작동 방식

1. **Title Data에 루트 테이블 정의**: 드롭 테이블 구성(아이템 ID, 가중치, 수량)을 Title Data에 JSON으로 저장하면 코드를 다시 배포하지 않고도 업데이트할 수 있습니다.
2. **컨테이너를 가상 화폐로 표현**: 열지 않은 루트 상자 역할을 하는 가상 화폐인 고정 콘텐츠 컨테이너와 동일한 [컨테이너 토큰 패턴](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents)을 사용합니다.
3. **Azure Function을 호출해 컨테이너 열기**: 함수는 루트 테이블을 읽고 가중 무작위 선택을 실행한 다음 `ExecuteInventoryOperations`를 호출하여 컨테이너 토큰 1개를 원자적으로 차감하고 무작위로 선택된 아이템을 추가합니다.

## 필수 구성 요소

* [PlayFab 개발자 계정](https://developer.playfab.com/en-us/sign-up).
* [Game Manager](https://developer.playfab.com/)에서 만든 타이틀.
* 루트 보상으로 부여할 수 있는 게시된 카탈로그 아이템.
* 컨테이너를 나타내는 가상 화폐(설정 지침은 [고정 콘텐츠 컨테이너](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents#step-1-create-a-virtual-currency-for-the-container) 참조).
* PlayFab 타이틀에 연결된 [Azure Functions](/services/playfab/live-service-management/service-gateway/automation/cloudscript-af/quickstart) 프로젝트.

## 1단계: Title Data에 루트 테이블 정의

디자이너가 코드 변경 없이 드롭 확률을 업데이트할 수 있도록 [Title Data](/services/playfab/live-service-management/game-configuration/titledata)에 루트 테이블 정의를 저장합니다.

### 예제 루트 테이블 구조

JSON 값을 갖는 `LootTables`라는 Title Data 키를 만듭니다.

```json theme={null}
{
  "TreasureChest": {
    "Rolls": 1,
    "Items": [
      { "ItemId": "{{CommonSwordID}}", "Weight": 1000, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{UncommonSwordID}}", "Weight": 100, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{RareSwordID}}", "Weight": 10, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{LegendarySwordID}}", "Weight": 1, "MinQuantity": 1, "MaxQuantity": 1 }
    ]
  },
  "SupplyCrate": {
    "Rolls": 3,
    "Items": [
      { "ItemId": "{{HealthPotionID}}", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 5 },
      { "ItemId": "{{ManaPotionID}}", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 3 },
      { "ItemId": "{{GoldCurrencyID}}", "Weight": 80, "MinQuantity": 10, "MaxQuantity": 100 },
      { "ItemId": "{{RareGemID}}", "Weight": 5, "MinQuantity": 1, "MaxQuantity": 1 }
    ]
  }
}
```

각 루트 테이블에는 다음 속성이 포함됩니다.

| 속성                            | 설명                                      |
| ----------------------------- | --------------------------------------- |
| **Rolls**                     | 컨테이너를 열 때 테이블에서 롤할 횟수.                  |
| **Items**                     | 가능한 보상 풀이며 각각 가중치와 수량 범위를 가집니다.         |
| **Weight**                    | 상대 확률입니다. 가중치가 높을수록 아이템이 드롭될 가능성이 높습니다. |
| **MinQuantity / MaxQuantity** | 롤당 부여할 해당 아이템의 수량 범위.                   |

### Title Data 설정

#### Game Manager

1. **Content** > **Title Data**로 이동합니다.
2. `LootTables`라는 키를 만듭니다.
3. JSON 루트 테이블 구성을 값으로 붙여넣습니다.
4. **Save**를 선택합니다.

#### API

[SetTitleData](https://learn.microsoft.com/en-us/rest/api/playfab/server/title-wide-data-management/set-title-data)를 사용합니다.

```json theme={null}
{
  "Key": "LootTables",
  "Value": "{\"TreasureChest\":{\"Rolls\":1,\"Items\":[...]}}"
}
```

***

## 2단계: Azure Function 만들기

플레이어가 컨테이너를 열 때 게임 클라이언트가 호출하는 Azure Function을 만듭니다. 함수는 다음을 수행합니다.

1. 플레이어가 컨테이너 토큰을 하나 이상 갖고 있는지 확인합니다.
2. Title Data에서 루트 테이블을 읽습니다.
3. 가중 무작위 선택을 실행합니다.
4. `ExecuteInventoryOperations`를 호출하여 토큰을 원자적으로 차감하고 보상을 부여합니다.

### 데이터 모델

```csharp theme={null}
public class OpenContainerRequest
{
    public string LootTableId { get; set; }  // e.g., "TreasureChest"
}

public class LootTable
{
    public int Rolls { get; set; }
    public List<LootTableEntry> Items { get; set; }
}

public class LootTableEntry
{
    public string ItemId { get; set; }
    public int Weight { get; set; }
    public int MinQuantity { get; set; }
    public int MaxQuantity { get; set; }
}
```

### 가중 무작위 선택

```csharp theme={null}
public static LootTableEntry RollOnTable(LootTable table, Random rng)
{
    int totalWeight = table.Items.Sum(e => e.Weight);
    int roll = rng.Next(0, totalWeight);

    int cumulative = 0;
    foreach (var entry in table.Items)
    {
        cumulative += entry.Weight;
        if (roll < cumulative)
            return entry;
    }

    // Fallback (shouldn't reach here)
    return table.Items.Last();
}
```

### 전체 Azure Function

```csharp theme={null}
[FunctionName("OpenLootContainer")]
public static async Task<dynamic> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    // 1. Parse the PlayFab context and request
    var context = JsonConvert.DeserializeObject<FunctionExecutionContext<dynamic>>(
        await req.ReadAsStringAsync());
    var args = context.FunctionArgument != null
        ? JsonConvert.DeserializeObject<OpenContainerRequest>(
            context.FunctionArgument.ToString())
        : null;

    if (args == null || string.IsNullOrEmpty(args.LootTableId))
        return new { Error = "LootTableId is required." };

    var playerEntity = context.CallerEntityProfile.Entity;
    var settings = new PlayFabApiSettings
    {
        TitleId = context.TitleAuthenticationContext.Id,
        DeveloperSecretKey = Environment.GetEnvironmentVariable("PLAYFAB_DEV_SECRET_KEY")
    };
    var authContext = new PlayFabAuthenticationContext
    {
        EntityToken = context.TitleAuthenticationContext.EntityToken
    };

    var serverApi = new PlayFabServerInstanceAPI(settings);
    var economyApi = new PlayFabEconomyInstanceAPI(settings, authContext);

    // 2. Read the loot table from Title Data
    var titleDataResult = await serverApi.GetTitleDataAsync(new GetTitleDataRequest
    {
        Keys = new List<string> { "LootTables" }
    });

    if (!titleDataResult.Result.Data.ContainsKey("LootTables"))
        return new { Error = "LootTables not found in Title Data." };

    var allTables = JsonConvert.DeserializeObject<Dictionary<string, LootTable>>(
        titleDataResult.Result.Data["LootTables"]);

    if (!allTables.ContainsKey(args.LootTableId))
        return new { Error = $"Loot table '{args.LootTableId}' not found." };

    var lootTable = allTables[args.LootTableId];

    // 3. Roll for rewards
    var rng = new Random();
    var rewards = new Dictionary<string, int>(); // ItemId -> total quantity

    for (int i = 0; i < lootTable.Rolls; i++)
    {
        var entry = RollOnTable(lootTable, rng);
        int quantity = rng.Next(entry.MinQuantity, entry.MaxQuantity + 1);

        if (rewards.ContainsKey(entry.ItemId))
            rewards[entry.ItemId] += quantity;
        else
            rewards[entry.ItemId] = quantity;
    }

    // 4. Build the atomic operation: subtract 1 container token + add rewards
    var operations = new List<InventoryOperation>();

    // Subtract the container token
    // The container currency ID must match the LootTableId mapping
    // Store this mapping in Title Data or use a naming convention
    operations.Add(new InventoryOperation
    {
        Subtract = new SubtractInventoryItemsOperation
        {
            Item = new InventoryItemReference
            {
                Id = GetContainerCurrencyId(args.LootTableId) // Your mapping logic
            },
            Amount = 1
        }
    });

    // Add each reward
    foreach (var reward in rewards)
    {
        operations.Add(new InventoryOperation
        {
            Add = new AddInventoryItemsOperation
            {
                Item = new InventoryItemReference { Id = reward.Key },
                Amount = reward.Value
            }
        });
    }

    // 5. Execute atomically
    var executeResult = await economyApi.ExecuteInventoryOperationsAsync(
        new ExecuteInventoryOperationsRequest
        {
            Entity = new EntityKey
            {
                Id = playerEntity.Id,
                Type = playerEntity.Type
            },
            Operations = operations,
            IdempotencyId = Guid.NewGuid().ToString()
        });

    if (executeResult.Error != null)
    {
        log.LogError($"Failed to execute operations: {executeResult.Error.ErrorMessage}");
        return new { Error = executeResult.Error.ErrorMessage };
    }

    log.LogInformation($"Player {playerEntity.Id} opened {args.LootTableId}, received: " +
        string.Join(", ", rewards.Select(r => $"{r.Value}x {r.Key}")));

    return new { Rewards = rewards, TransactionIds = executeResult.Result.TransactionIds };
}
```

<Note>
  `GetContainerCurrencyId` 도우미 함수는 루트 테이블 이름을 해당 컨테이너 화폐 ID에 매핑합니다. 이 매핑은 루트 테이블과 함께 Title Data에 저장하거나 명명 규칙을 사용할 수 있습니다.
</Note>

## 3단계: 플레이어에게 컨테이너 토큰 부여

게임플레이, 스토어 구매 또는 보상을 통해 플레이어에게 컨테이너 토큰을 부여합니다. [고정 콘텐츠 컨테이너](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents#step-3-grant-container-tokens-to-players)에서 설명한 것과 동일한 접근 방식을 사용합니다.

## 4단계: 게임 클라이언트에서 함수 호출

플레이어가 컨테이너를 열 때 [ExecuteFunction](https://learn.microsoft.com/en-us/rest/api/playfab/cloudscript/server-side-cloud-script/execute-function)을 사용해 Azure Function을 호출합니다.

```json theme={null}
{
    "FunctionName": "OpenLootContainer",
    "FunctionParameter": {
        "LootTableId": "TreasureChest"
    },
    "GeneratePlayStreamEvent": true
}
```

응답에는 무작위로 선택된 보상이 포함됩니다.

```json theme={null}
{
    "FunctionResult": {
        "Rewards": {
            "{{UncommonSwordID}}": 1
        },
        "TransactionIds": ["txn-abc-123"]
    }
}
```

## 루트 테이블 설계 패턴

### 희귀도 등급

기하급수적으로 감소하는 가중치를 가진 고전적인 희귀도 시스템:

```json theme={null}
{
  "Items": [
    { "ItemId": "CommonItem",     "Weight": 1000, "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "UncommonItem",   "Weight": 100,  "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "RareItem",       "Weight": 10,   "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "LegendaryItem",  "Weight": 1,    "MinQuantity": 1, "MaxQuantity": 1 }
  ]
}
```

드롭 확률:

* Common: \~90.0%
* Uncommon: \~9.0%
* Rare: \~0.9%
* Legendary: \~0.09%

### 혼합 보상을 사용한 다중 롤

혼합 풀에서 3개의 무작위 아이템을 제공하는 보급품 상자:

```json theme={null}
{
  "Rolls": 3,
  "Items": [
    { "ItemId": "{{HealthPotionID}}", "Weight": 40, "MinQuantity": 1, "MaxQuantity": 3 },
    { "ItemId": "{{ManaPotionID}}",   "Weight": 40, "MinQuantity": 1, "MaxQuantity": 3 },
    { "ItemId": "{{GoldCurrencyID}}", "Weight": 30, "MinQuantity": 10, "MaxQuantity": 50 },
    { "ItemId": "{{RareGemID}}",      "Weight": 5,  "MinQuantity": 1, "MaxQuantity": 1 }
  ]
}
```

### 보장 + 무작위 (피티 시스템)

"10번 열 때마다 레어 이상 보장" 패턴의 경우, [Display Properties](/services/playfab/economy-monetization/economy-v2/inventory#display-properties) 또는 [Player Data](/services/playfab/player-progression/player-data/quickstart)에서 플레이어의 열기 횟수를 추적하고, 임계값에 도달했을 때 Azure Function에서 무작위 선택을 재정의합니다.

### 중첩된 드롭 테이블

루트 테이블은 ID로 다른 루트 테이블을 참조할 수 있습니다. Azure Function은 참조를 재귀적으로 확인합니다.

```json theme={null}
{
  "BossDropTable": {
    "Rolls": 2,
    "Items": [
      { "ItemId": "TABLE:WeaponDropTable", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "TABLE:ArmorDropTable",  "Weight": 30, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{GoldCurrencyID}}",    "Weight": 80, "MinQuantity": 50, "MaxQuantity": 200 }
    ]
  }
}
```

함수는 `ItemId`가 `TABLE:`로 시작하는지 확인하고, 그렇다면 아이템을 직접 부여하는 대신 참조된 테이블에서 롤합니다.

## 보안 고려 사항

* **항상 루트 로직을 서버 측 Azure Function에서 실행**합니다. 어떤 아이템이 롤되었는지 결정하는 데 클라이언트를 절대 신뢰하지 마세요. 이 접근 방식은 조작을 방지합니다.
* 함수가 재시도되는 경우 중복 부여를 방지하려면 `ExecuteInventoryOperations` 호출에 **`IdempotencyId`를 사용**하세요. 자세한 내용은 [멱등성 트랜잭션 및 재시도](/services/playfab/economy-monetization/economy-v2/tutorials/idempotent-transactions-and-retries)를 참조하세요.
* 차감 전에 플레이어의 인벤토리를 확인하여 **컨테이너 소유권을 확인**합니다. `ExecuteInventoryOperations`의 `Subtract` 작업은 플레이어에게 토큰이 충분하지 않으면 실패하며, 전체 배치를 롤백합니다.
* 감사를 위해 **모든 롤을 로깅**합니다. 함수의 반환 값과 PlayStream 이벤트는 각 플레이어가 받은 것에 대한 추적을 제공합니다.

## 참고 항목

* [고정 콘텐츠 컨테이너](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents)
* [ExecuteInventoryOperations API 참조](https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/execute-inventory-operations)
* [멱등성 트랜잭션 및 재시도](/services/playfab/economy-monetization/economy-v2/tutorials/idempotent-transactions-and-retries)
* [Title Data](/services/playfab/live-service-management/game-configuration/titledata)
* [Azure Functions 빠른 시작](/services/playfab/live-service-management/service-gateway/automation/cloudscript-af/quickstart)
* [드롭 테이블(레거시 Economy)](/services/playfab/economy-monetization/economy/tutorials/drop-tables)
* [Economy v2 개요](/services/playfab/economy-monetization/economy-v2/overview)
