> ## 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, Unity IAP, Android 시작하기

> PlayFab Economy v2 번들에 Unity IAP와 Android Billing API를 연결하여 플레이어가 실제 돈으로 아이템을 구매하고 인벤토리에 상환할 수 있도록 합니다.

# Economy v2, Unity, Android 시작하기

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

이 튜토리얼에서는 PlayFab, Unity + IAP 서비스, Android Billing API를 사용해 인앱 구매(IAP)를 설정하는 방법을 보여줍니다.

## 시작하기 전

Android Billing API와 PlayFab은 함께 작동하여 클라이언트에 IAP 경험을 제공합니다.

<img src="https://mintcdn.com/microsoft-4404708b/68AB2fedpk3M7e-n/images/playfab/economy-monetization/economy-v2/playfab-economyv2-redemption-timeline.png?fit=max&auto=format&n=68AB2fedpk3M7e-n&q=85&s=498ccac2bdd047ef019555cf9edfee34" alt="PlayFab Economy v2 - 상환 타임라인" width="855" height="390" data-path="images/playfab/economy-monetization/economy-v2/playfab-economyv2-redemption-timeline.png" />

PlayMarket에서 **제품 ID**와 **가격**을 설정하는 것으로 시작합니다. 처음에는 모든 제품이 *이름 없는* 것입니다. 즉, 플레이어가 구매할 수 있는 디지털 엔터티이지만 PlayFab 플레이어에게는 아무런 의미가 없습니다.

이러한 엔터티를 유용하게 만들려면 PlayFab 아이템 카탈로그에 미러링해야 합니다. PlayFab은 이름 없는 엔터티를 번들, 컨테이너, 개별 아이템으로 변환합니다.

각각은 다음과 같은 고유한 얼굴을 가집니다.

* **제목**
* **설명**
* **태그**
* **유형**
* **이미지**
* **동작**

모든 아이템은 ID를 공유하여 마켓 제품에 연결됩니다.

구매 가능한 실제 돈 아이템에 액세스하는 가장 좋은 방법은 [GetItems](https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog/get-items)를 사용하는 것입니다.

아이템의 ID는 PlayFab과 외부 IAP 시스템 간의 연결입니다. 따라서 IAP 서비스에 아이템 ID를 전달합니다.

이 시점에서 구매 프로세스가 시작됩니다. 플레이어는 IAP 인터페이스와 상호 작용하며, 구매가 성공하면 영수증을 받습니다.

PlayFab은 영수증을 검증하고 구매를 등록하여 PlayFab 플레이어에게 방금 구매한 아이템을 부여합니다.

## 클라이언트 애플리케이션 설정

이 섹션에서는 PlayFab, UnityIAP, Android Billing API를 사용해 IAP를 테스트하기 위한 애플리케이션을 구성하는 방법을 보여줍니다.

필수 구성 요소:

* Unity 프로젝트.
* 타이틀에서 작동하도록 가져오고 구성된 [PlayFab Unity SDK](/services/playfab/sdks/unity3d).
* Unity 프로젝트에서 작동하도록 설치되고 구성된 [Visual Studio](https://learn.microsoft.com/en-us/visualstudio/gamedev/unity/get-started/getting-started-with-visual-studio-tools-for-unity)와 같은 편집기.

첫 번째 단계는 UnityIAP를 설정하는 것입니다.

1. **Services**로 이동합니다.
2. **Services** 탭이 선택되어 있는지 확인합니다.
3. **Unity Services** 프로필 또는 조직을 선택합니다.
4. **Create** 버튼을 선택합니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-setting-up-service.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=c74b3cd003f67bb4998e4f0b36b24db6" alt="UnityIAP 서비스 설정" width="570" height="693" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-setting-up-service.png" />

1. 다음으로 **In-App Purchasing (IAP)** 서비스로 이동합니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-navigate-to-service.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=b5a4f77ba599c1368e2a9b7185ff2dfe" alt="UnityIAP 서비스로 이동" width="500" height="862" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-navigate-to-service.png" />

1. **Simplify cross-platform IAP** 토글을 설정하여 **서비스**를 활성화해야 합니다.
2. 그런 다음 **Continue** 버튼을 선택합니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-enable-service.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=dbb412be4fe8ab0d42d8417b12f4113d" alt="UnityIAP 서비스 활성화" width="428" height="325" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-enable-service.png" />

플러그인 목록이 있는 페이지가 나타납니다.

1. **Import** 버튼을 선택합니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-import-plugins.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=9c25924c7488ebef1c63f914fad0ab60" alt="UnityIAP 서비스 - 플러그인 가져오기" width="444" height="525" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-import-plugins.png" />

Unity 설치를 계속 진행하고 모든 플러그인을 가져올 때까지 가져오기 절차를 진행합니다.

1. 플러그인이 제자리에 있는지 확인합니다.
2. 그런 다음 **AndroidIAPExample.cs**라는 새 스크립트를 만듭니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-create-new-script.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=3bca6b55a46e1a1ef00ee671a9e47c90" alt="UnityIAP 새 스크립트 만들기" width="407" height="330" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-create-new-script.png" />

`AndroidIAPExample.cs`에는 다음 코드가 포함됩니다(자세한 내용은 코드 주석을 참조).

```csharp theme={null}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;

using PlayFab;
using PlayFab.ClientModels;
using PlayFab.EconomyModels;

/// <summary>
/// Unity behavior that implements the Unity IAP Store interface.
/// Attach as an asset to your Scene.
/// </summary>
public class AndroidIAPExample : MonoBehaviour, IDetailedStoreListener
{
    // Bundles for sale on the Google Play Store.
    private Dictionary<string, PlayFab.EconomyModels.CatalogItem> _googlePlayCatalog;

    // In-game items for sale at the example vendor.
    private Dictionary<string, PlayFab.EconomyModels.CatalogItem> _storefrontCatalog;

    private string _purchaseIdempotencyId = null;

    private PlayFabEconomyAPIAsyncResult _lastAPICallResult = null;

    private static readonly PlayFabEconomyAPIAsync s_economyAPI = new();

    private static IStoreController s_storeController;

    // TODO: This callback is for illustrations purposes, you should create one that fits your needs
    public delegate void PlayFabProcessPurchaseCallback(PurchaseProcessingResult result);

    /// <summary>
    /// Event that is triggered when a purchase is processed.
    /// </summary>
    /// <remarks>
    /// TODO: Subscribe to this event in your game code to handle purchase results.
    /// </remarks>
    public event PlayFabProcessPurchaseCallback PlayFabProcessPurchaseEvent;

    /// <summary>
    /// True if the Store Controller, extensions, and Catalog are set.
    /// </summary>
    public bool IsInitialized => s_storeController != null
                             && _googlePlayCatalog != null
                             && _storefrontCatalog != null;

    // Start is called before the first frame update.
    public void Start()
    {
        Login();
    }

    /// <summary>
    /// Attempts to log the player in via the Android Device ID.
    /// </summary>
    private void Login()
    {
        // TODO: it is better to use LoginWithGooglePlayGamesService or a similar platform-specific login method for final game code.

        // SystemInfo.deviceUniqueIdentifier will prompt for permissions on newer devices.
        // Using a non-device specific GUID and saving to a local file
        // is a better approach. PlayFab does allow you to link multiple
        // Android device IDs to a single PlayFab account.
        PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest()
        {
            CreateAccount = true,
            CustomId = SystemInfo.deviceUniqueIdentifier
        }, result => RefreshIAPItems(), PlayFabSampleUtil.OnPlayFabError);
    }

    /// <summary>
    /// Queries the PlayFab Economy Catalog V2 for updated listings
    /// and then fills the local catalog objects.
    /// </summary>
    private async void RefreshIAPItems()
    {
        _googlePlayCatalog = new Dictionary<string, PlayFab.EconomyModels.CatalogItem>();
        SearchItemsRequest googlePlayCatalogRequest = new()
        {
            Count = 50,
            Filter = "AlternateIds/any(t: t/type eq 'GooglePlay')"
        };

        SearchItemsResponse googlePlayCatalogResponse;
        do
        {
            googlePlayCatalogResponse = await s_economyAPI.SearchItemsAsync(googlePlayCatalogRequest);
            Debug.Log("Search response: " + JsonUtility.ToJson(googlePlayCatalogResponse));

            foreach (PlayFab.EconomyModels.CatalogItem item in googlePlayCatalogResponse.Items)
            {
                _googlePlayCatalog.Add(item.Id, item);
            }

        } while (!string.IsNullOrEmpty(googlePlayCatalogResponse.ContinuationToken));

        Debug.Log($"Completed pulling from PlayFab Economy v2 googleplay Catalog: {_googlePlayCatalog.Count()} items retrieved");

        _storefrontCatalog = new Dictionary<string, PlayFab.EconomyModels.CatalogItem>();
        GetItemRequest storeCatalogRequest = new()
        {
            AlternateId = new CatalogAlternateId()
            {
                Type = "FriendlyId",
                Value = "villagerstore"
            }
        };

        GetItemResponse storeCatalogResponse;
        storeCatalogResponse = await s_economyAPI.GetItemAsync(storeCatalogRequest);
        List<string> itemIds = new();

        foreach (CatalogItemReference item in storeCatalogResponse.Item.ItemReferences)
        {
            itemIds.Add(item.Id);
        }

        GetItemsRequest itemsCatalogRequest = new()
        {
            Ids = itemIds
        };

        GetItemsResponse itemsCatalogResponse = await s_economyAPI.GetItemsAsync(itemsCatalogRequest);
        foreach (PlayFab.EconomyModels.CatalogItem item in itemsCatalogResponse.Items)
        {
            _storefrontCatalog.Add(item.Id, item);
        }

        Debug.Log($"Completed pulling from PlayFab Economy v2 villagerstore store: {_storefrontCatalog.Count()} items retrieved");

        InitializePurchasing();
    }

    /// <summary>
    /// Initializes the Unity IAP system for the Google Play Store.
    /// </summary>
    private void InitializePurchasing()
    {
        if (IsInitialized) return;

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(AppStore.GooglePlay));

        foreach (PlayFab.EconomyModels.CatalogItem item in _googlePlayCatalog.Values)
        {
            string googlePlayItemId = item.AlternateIds.FirstOrDefault(item => item.Type == "GooglePlay")?.Value;
            if (!string.IsNullOrWhiteSpace(googlePlayItemId))
            {
                builder.AddProduct(googlePlayItemId, ProductType.Consumable);
            }
        }

        UnityPurchasing.Initialize(this, builder);
    }

    /// <summary>
    /// Draw a debug IMGUI for testing examples.
    /// Use UI Toolkit for your production game runtime UI instead.
    /// </summary>
    public void OnGUI()
    {
        // Support high-res devices.
        GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, new Vector3(3, 3, 3));

        if (!IsInitialized)
        {
            GUILayout.Label("Initializing IAP and logging in...");
            return;
        }

        if (!string.IsNullOrEmpty(_purchaseIdempotencyId) && (!string.IsNullOrEmpty(_lastAPICallResult?.Message)
                                                           || !string.IsNullOrEmpty(_lastAPICallResult?.Error)))
        {
            GUILayout.Label(_lastAPICallResult?.Message + _lastAPICallResult?.Error);
        }

        GUILayout.Label("Shop for game currency bundles.");
        // Draw a purchase menu for each catalog item.
        foreach (PlayFab.EconomyModels.CatalogItem item in _googlePlayCatalog.Values)
        {
            // Use a dictionary to select the proper language.
            if (GUILayout.Button("Get " + (item.Title.ContainsKey("en-US") ? item.Title["en-US"] : item.Title["NEUTRAL"])))
            {
                BuyProductById(item.AlternateIds.FirstOrDefault(item => item.Type == "GooglePlay").Value);
            }
        }

        GUILayout.Label("Hmmm. (Translation: Welcome to my humble Villager store.)");
        // Draw a purchase menu for each catalog item.
        foreach (PlayFab.EconomyModels.CatalogItem item in _storefrontCatalog.Values)
        {
            // Use a dictionary to select the proper language.
            if (GUILayout.Button("Buy "
                + (item.Title.ContainsKey("en-US") ? item.Title["en-US"] : item.Title["NEUTRAL"]
                + ": "
                + item.PriceOptions.Prices.FirstOrDefault().Amounts.FirstOrDefault().Amount.ToString()
                + " Diamonds"
                )))
            {
                Task.Run(() => PlayFabPurchaseItemById(item.Id));
            }
        }
    }

    /// <summary>
    /// Integrates game purchasing with the Unity IAP API.
    /// </summary>
    public void BuyProductById(string productId)
    {
        if (!IsInitialized)
        {
            Debug.LogError("IAP Service is not initialized!");
            return;
        }

        s_storeController.InitiatePurchase(productId);
    }

    /// <summary>
    /// Purchases a PlayFab inventory item by ID.
    /// See the <see cref="PlayFabEconomyAPIAsync"/> class for details on error handling
    /// and calling patterns.
    /// </summary>
    async public Task<bool> PlayFabPurchaseItemById(string itemId)
    {
        if (!IsInitialized)
        {
            Debug.LogError("IAP Service is not initialized!");
            return false;
        }

        _lastAPICallResult = new();

        Debug.Log("Player buying product " + itemId);

        if (string.IsNullOrEmpty(_purchaseIdempotencyId))
        {
            _purchaseIdempotencyId = Guid.NewGuid().ToString();
        }

        GetItemRequest getVillagerStoreRequest = new()
        {
            AlternateId = new CatalogAlternateId()
            {
                Type = "FriendlyId",
                Value = "villagerstore"
            }
        };

        GetItemResponse getStoreResponse = await s_economyAPI.GetItemAsync(getVillagerStoreRequest);
        if (getStoreResponse == null || string.IsNullOrEmpty(getStoreResponse?.Item?.Id))
        {
            _lastAPICallResult.Error = "Unable to contact the store. Check your internet connection and try again in a few minutes.";
            return false;
        }

        CatalogPriceAmount price = _storefrontCatalog.FirstOrDefault(item => item.Key == itemId).Value.PriceOptions.Prices.FirstOrDefault().Amounts.FirstOrDefault();
        PurchaseInventoryItemsRequest purchaseInventoryItemsRequest = new()
        {
            Amount = 1,
            Item = new InventoryItemReference()
            {
                Id = itemId
            },
            PriceAmounts = new List<PurchasePriceAmount>
            {
                new()
                {
                    Amount = price.Amount,
                    ItemId = price.ItemId
                }
            },
            IdempotencyId = _purchaseIdempotencyId,
            StoreId = getStoreResponse.Item.Id
        };

        PurchaseInventoryItemsResponse purchaseInventoryItemsResponse = await s_economyAPI.PurchaseInventoryItemsAsync(purchaseInventoryItemsRequest);
        if (purchaseInventoryItemsResponse == null || purchaseInventoryItemsResponse?.TransactionIds.Count < 1)
        {
            _lastAPICallResult.Error = "Unable to purchase. Try again in a few minutes.";
            return false;
        }

        _purchaseIdempotencyId = "";
        _lastAPICallResult.Message = "Purchasing!";
        return true;
    }

    private void OnRegistration(LoginResult result)
    {
        PlayFabSettings.staticPlayer.ClientSessionTicket = result.SessionTicket;
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        s_storeController = controller;

        extensions.GetExtension<IGooglePlayStoreExtensions>().RestoreTransactions((result, error) => {
            if (result)
            {
                Debug.LogWarning("Restore transactions succeeded.");
            }
            else
            {
                Debug.LogWarning("Restore transactions failed.");
            }
        });
    }

    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    }

    public void OnInitializeFailed(InitializationFailureReason error, string message)
    {
        Debug.Log("OnInitializeFailed InitializationFailureReason:" + error + message);
    }

    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureReason failureReason)
    {
        Debug.Log($"OnPurchaseFailed: FAIL. Product: '{product.definition.storeSpecificId}', PurchaseFailureReason: {failureReason}");
    }

    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureDescription failureDescription)
    {
        Debug.Log($"OnPurchaseFailed: FAIL. Product: '{product.definition.storeSpecificId}', PurchaseFailureReason: {failureDescription}");
    }

    /// <summary>
    /// Callback for Store purchases. Subscribe to PlayFabProcessPurchaseEvent to handle the final PurchaseProcessingResult.
    /// <see href="https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/api/UnityEngine.Purchasing.PurchaseProcessingResult.html"/>
    /// </summary>
    /// <remarks>
    /// This code does not account for purchases that were pending and are
    /// delivered on application start. Production code should account for these cases.
    /// </remarks>
    /// <returns>Complete immediately upon error. Pending if PlayFab Economy is handling final processing and will trigger PlayFabProcessPurchaseEvent with the final result.</returns>
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {
        if (!IsInitialized)
        {
            Debug.LogWarning("Not initialized. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        if (purchaseEvent.purchasedProduct == null)
        {
            Debug.LogWarning("Attempted to process purchase with unknown product. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        if (string.IsNullOrEmpty(purchaseEvent.purchasedProduct.receipt))
        {
            Debug.LogWarning("Attempted to process purchase with no receipt. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        Debug.Log("Attempting purchase with receipt " + purchaseEvent.purchasedProduct.receipt);
        GooglePurchase purchasePayload = GooglePurchase.FromJson(purchaseEvent.purchasedProduct.receipt);
        RedeemGooglePlayInventoryItemsRequest request = new()
        {
            Purchases = new List<GooglePlayProductPurchase>
            {
                new()
                {
                    ProductId = purchasePayload.PayloadData?.JsonData?.productId,
                    Token = purchasePayload.PayloadData?.JsonData?.purchaseToken
                }
            }
        };

        PlayFabEconomyAPI.RedeemGooglePlayInventoryItems(request, result =>
        {
            Debug.Log("Processed receipt validation.");

            if (result?.Failed.Count > 0)
            {
                Debug.Log($"Validation failed for {result.Failed.Count} receipts.");
                Debug.Log(JsonUtility.ToJson(result.Failed));
                PlayFabProcessPurchaseEvent?.Invoke(PurchaseProcessingResult.Pending);
            }
            else
            {
                Debug.Log("Validation succeeded!");
                PlayFabProcessPurchaseEvent?.Invoke(PurchaseProcessingResult.Complete);
                s_storeController.ConfirmPendingPurchase(purchaseEvent.purchasedProduct);
                Debug.Log("Confirmed purchase with Google Marketplace.");
            }
        },
        PlayFabSampleUtil.OnPlayFabError);

        return PurchaseProcessingResult.Pending;
    }
}

/// <summary>
/// Utility classes for the sample.
/// </summary>
public class PlayFabEconomyAPIAsyncResult
{
    public string Error { get; set; } = null;

    public string Message { get; set; } = null;
}

public static class PlayFabSampleUtil
{
    public static void OnPlayFabError(PlayFabError error)
    {
        Debug.LogError(error.GenerateErrorReport());
    }
}

/// <summary>
/// Example Async wrapper for PlayFab API's.
/// 
/// This is just a quick sample for example purposes.
/// 
/// Write your own customer Logger implementation to log and handle errors
/// for user-facing scenarios. Use tags and map which PlayFab errors require your
/// game to handle GUI or gameplay updates vs which should be logged to crash and
/// error reporting services.
/// </summary>
public class PlayFabEconomyAPIAsync
{
    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/get-item"/>
    /// </summary>
    public Task<GetItemResponse> GetItemAsync(GetItemRequest request)
    {
        TaskCompletionSource<GetItemResponse> getItemAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItem(request, (response) => getItemAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            getItemAsyncTaskSource.SetResult(default);
        });
        return getItemAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/get-items"/>
    /// </summary>
    public Task<GetItemsResponse> GetItemsAsync(GetItemsRequest request)
    {
        TaskCompletionSource<GetItemsResponse> getItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItems(request, (response) => getItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            getItemsAsyncTaskSource.SetResult(default);
        });
        return getItemsAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/inventory/purchase-inventory-items"/>
    /// </summary>
    public Task<PurchaseInventoryItemsResponse> PurchaseInventoryItemsAsync(PurchaseInventoryItemsRequest request)
    {
        TaskCompletionSource<PurchaseInventoryItemsResponse> purchaseInventoryItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.PurchaseInventoryItems(request, (response) => purchaseInventoryItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            purchaseInventoryItemsAsyncTaskSource.SetResult(default);
        });
        return purchaseInventoryItemsAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/search-items"/>
    /// </summary>
    public Task<SearchItemsResponse> SearchItemsAsync(SearchItemsRequest request)
    {
        TaskCompletionSource<SearchItemsResponse> searchItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.SearchItems(request, (response) => searchItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            searchItemsAsyncTaskSource.SetResult(default);
        });
        return searchItemsAsyncTaskSource.Task;
    }
}

[Serializable]
public class PurchaseJsonData
{
    public string orderId;
    public string packageName;
    public string productId;
    public string purchaseToken;
    public long   purchaseTime;
    public int    purchaseState;
}

[Serializable]
public class PurchasePayloadData
{
    public PurchaseJsonData JsonData;

    public string signature;
    public string json;

    public static PurchasePayloadData FromJson(string json)
    {
        var payload = JsonUtility.FromJson<PurchasePayloadData>(json);
        payload.JsonData = JsonUtility.FromJson<PurchaseJsonData>(payload.json);
        return payload;
    }
}

[Serializable]
public class GooglePurchase
{
    public PurchasePayloadData PayloadData;

    public string Store;
    public string TransactionID;
    public string Payload;

    public static GooglePurchase FromJson(string json)
    {
        var purchase = JsonUtility.FromJson<GooglePurchase>(json);

        // Only fake receipts are returned in Editor play.
        if (Application.isEditor)
        {
            return purchase;
        }

        purchase.PayloadData = PurchasePayloadData.FromJson(purchase.Payload);
        return purchase;
    }
}
```

1. **Code**라는 새 **GameObject**를 만듭니다.
2. `AndroidIAPExample` 컴포넌트를 여기에 추가합니다(끌어다 놓기).
3. 씬을 반드시 **저장**합니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-create-new-script.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=3bca6b55a46e1a1ef00ee671a9e47c90" alt="UnityIAP 예제 게임 오브젝트 만들기" width="407" height="330" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-create-new-script.png" />

마지막으로 **Build Settings**로 이동합니다.

1. 씬이 **Scenes In Build** 영역에 추가되었는지 확인합니다.
2. **Android** 플랫폼이 선택되었는지 확인합니다.
3. **Player Settings** 영역으로 이동합니다.
4. **Package Name**을 지정합니다.

<Note>
  **PlayMarket** 충돌을 피하기 위해 반드시 *자체* 패키지 이름을 만들어야 합니다.
</Note>

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-add-example-game-object.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=3475d7ac21193c6f4d8056e600146400" alt="UnityIAP 예제 게임 오브젝트 추가" width="1089" height="749" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-add-example-game-object.png" />

마지막으로 애플리케이션을 평소대로 빌드하고 APK가 있는지 확인합니다.

테스트하려면 PlayMarket과 PlayFab을 구성해야 합니다.

## IAP용 PlayMarket 애플리케이션 설정

이 섹션에서는 PlayMarket 애플리케이션에 대해 IAP를 활성화하는 방법의 세부 사항을 설명합니다.

<Note>
  애플리케이션 자체를 설정하는 것은 이 튜토리얼의 범위를 벗어납니다. 이미 애플리케이션이 있고 최소한 Alpha 릴리스를 게시하도록 구성되어 있다고 가정합니다.
</Note>

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-enable-playmarket-application.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=471e393f3ea7553f6af3b71ad48d0d42" alt="PlayMarket 애플리케이션 활성화" width="608" height="157" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-enable-playmarket-application.png" />

유용한 참고 사항:

* 해당 지점에 도달하려면 APK가 업로드되어 있어야 합니다. 이전 섹션에서 만든 APK를 사용하세요.
* IAP 샌드박스를 활성화하려면 APK를 *Alpha* 또는 *Beta* 애플리케이션으로 업로드하세요.
* **Content Rating**을 구성하는 것은 애플리케이션에서 IAP가 어떻게 활성화되는지에 대한 질문을 포함합니다.
* PlayMarket은 게시자가 IAP를 사용하거나 테스트하는 것을 *허용하지* 않습니다. 테스트 목적으로 *다른* Google 계정을 선택하고 Alpha/Beta 빌드의 테스터로 추가하세요.

1. 애플리케이션 빌드를 게시합니다.
2. 메뉴에서 **In-app products**를 선택합니다.
   * **Merchant Account**를 요청받으면 연결하거나 만듭니다.
3. **Add New Product** 버튼을 선택합니다. <img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-new-product.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=c1456178bcaf5cb88175f567c9d59ad8" alt="PlayMarket 새 제품 추가" width="1151" height="640" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-new-product.png" />
4. 새 제품 화면에서 **Managed Product**를 선택합니다.
5. `100diamonds`와 같은 설명적인 \_Product ID\_를 지정합니다.
6. **Continue**를 선택합니다. <img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-id.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=271b7344e7069d7f072e6ef975d574bf" alt="PlayMarket 제품 ID 추가" width="639" height="482" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-id.png" />
7. PlayMarket은 예를 들어 `100 Diamonds`와 `A pack of 100 diamonds to spend in-game`과 같은 \*\*Title (1)\*\*과 \*\*Description (2)\*\*을 입력하도록 요구합니다. **Data Item** 데이터는 오로지 PlayFab 서비스에서만 제공되며, ID만 일치하면 됩니다. <img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-title-description.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=d35ca3f5054934ee2e0805a743e95538" alt="PlayMarket 제품 제목 설명 추가" width="852" height="511" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-title-description.png" />
8. 아래로 스크롤하고 **Add a price** 버튼을 선택합니다. <img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-price.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=904804be3624244921d814200f47bf77" alt="PlayMarket 제품 가격 추가" width="850" height="378" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-price.png" />
9. "\$0.99"와 같은 유효한 가격을 입력합니다(가격이 각 국가/지역별로 독립적으로 변환되는 방식에 유의).
10. **Apply** 버튼을 선택합니다. <img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-apply-local-prices.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=2c65bd319208f9c6fc0dedd3f2d6c4af" alt="PlayMarket 제품 로컬 가격 적용" width="897" height="779" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-add-product-apply-local-prices.png" />
11. 마지막으로 화면 상단으로 다시 스크롤하고 아이템 상태를 **Active**로 변경합니다. <img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-make-product-active.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=efd16c08827556f36b5f5921fdf2f338" alt="PlayMarket 제품 활성화" width="671" height="417" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-make-product-active.png" />
12. PlayFab을 PlayMarket과 연결하기 위해 **Licensing Key**를 저장합니다.
13. 메뉴에서 **Services & APIs**로 이동합니다.
14. 그런 다음 **Key**의 **Base64** 버전을 찾아 저장합니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-save-product-licensing-key.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=6e1c10d2a1f96480ef9c080247e640f5" alt="PlayMarket 제품 라이선스 키 저장" width="717" height="350" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-save-product-licensing-key.png" />

다음 단계는 IAP 테스트를 활성화하는 것입니다. 샌드박스는 Alpha 및 Beta 빌드에 대해 자동으로 활성화되지만, 앱을 테스트할 권한이 있는 계정을 설정해야 합니다.

1. **Home**으로 이동합니다.
2. 왼쪽 메뉴에서 **Account details**를 찾아 선택합니다.
3. **License Testing** 영역을 찾습니다.
4. **Test Accounts**가 목록에 있는지 확인합니다.
5. **License Test Response**가 **RESPOND\_NORMALLY**로 설정되어 있는지 확인합니다.

설정을 적용하는 것을 잊지 마세요!

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-enable-iap-testing.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=87b433152c575bc304935d1980a7b011" alt="PlayMarket IAP 테스트 활성화" width="562" height="657" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-playmarket-enable-iap-testing.png" />

이 시점에서 통합의 Play Market 측 설정이 완료되어야 합니다.

## PlayFab 타이틀 설정

마지막 단계는 제품을 반영하고 Google Billing API와 통합하도록 PlayFab 타이틀을 구성하는 것입니다.

1. **Add-ons**를 선택합니다.
2. 그런 다음 **Google** 애드온을 선택합니다.

<img src="https://mintcdn.com/microsoft-4404708b/mLCHf0iQv3VidfBe/images/playfab/identity/player-identity/platform-specific-authentication/tutorials/google-html5/open-google-add-on.png?fit=max&auto=format&n=mLCHf0iQv3VidfBe&q=85&s=1b0af874b0d7e46cf30337e322f0004a" alt="PlayFab Google 애드온 열기" width="1589" height="1043" data-path="images/playfab/identity/player-identity/platform-specific-authentication/tutorials/google-html5/open-google-add-on.png" />

1. **Package ID**를 입력합니다.
2. 이전 섹션에서 얻은 **Google App License Key**를 입력합니다.
3. **Install Google** 버튼을 선택하여 변경 사항을 커밋합니다.

다음 단계는 PlayFab에서 100 Diamonds 번들을 반영하는 것입니다.

### Game Manager

1. 새 Economy Catalog (V2) Currency를 만듭니다.
2. **Title**을 편집하고 **Description**을 추가합니다. 예를 들어, `Diamonds`, `Our in-game currency of choice.`
3. 화폐를 더 쉽게 찾기 위해 **Friendly ID** `diamonds`를 추가합니다.
4. **Save and publish**를 선택하여 변경 사항을 완료합니다.
5. **Currency** 목록에서 화폐를 확인합니다.
6. 다음으로, 새 Economy Catalog (V2) Bundle을 만듭니다.
7. **Title**을 편집하고 **Description**을 추가합니다. 예를 들어, `100 Diamonds Bundle`, `A pack of 100 diamonds to spend in-game.`
   ```JSON theme={null}
   {
       "NEUTRAL": "100 Diamonds Bundle",
       "en-US": "100 Diamonds Bundle",
       "en-GB": "100 Diamonds Bundle",
       "de-DE": "100 Diamantenbüschel"
   }
   ```

<Note>
  이 데이터는 **Play Market Item Title** 및 **Description**과 \_아무런 관련이 없다\_는 점을 명심하세요. 독립적입니다.
</Note>

1. Content type을 사용해 번들을 정리할 수 있습니다. 예를 들어, `appstorebundles`. Content type은 ⚙️ > Title Settings > Economy (V2)에서 관리됩니다.
2. 표시 속성에 지역화된 가격을 추가하여 실제 가격을 추적합니다.
   ```json theme={null}
   {
       "prices": [
           "en-us": 0.99,
           "en-gb": 0.85,
           "de-de": 0.45
       ]
   }
   ```
3. 번들에 새 아이템을 추가합니다. 필터에서 Currencies를 선택하고 이전 세트에서 만든 화폐를 선택합니다. 이 번들에서 판매하고자 하는 화폐 양과 일치하도록 수량을 설정합니다.
4. "GooglePlay" 마켓플레이스에 대한 새 플랫폼을 추가합니다. GooglePlay 마켓플레이스가 아직 없다면 Economy Settings 페이지에서 만들 수 있습니다. **Marketplace ID**를 이전 섹션에서 만든 Google Play Console Product ID와 일치하도록 설정합니다.
5. **Save and publish**를 선택하여 변경 사항을 완료합니다.
6. **Bundles** 목록에서 번들을 확인합니다.

### REST API

***

다음으로, 게임 내 NPC 상인을 나타내는 PlayFab 스토어를 통해 플레이어가 화폐를 소비할 수 있는 게임 내 구매를 설정할 수 있습니다.

### Game Manager

1. 새 Economy Catalog (V2) Item을 만듭니다.
2. **Title**을 편집하고 **Description**을 추가합니다. 예를 들어, "Golden Sword", "A sword made of gold."
3. 스토어에서 플레이어가 아이템을 찾도록 돕기 위해 지역화된 키워드를 추가할 수 있습니다. 나중에 API를 통한 검색을 위해 아이템을 정리하기 위해 Tag와 Content type을 추가합니다. Display Properties를 사용해 방어구 값, 아트 리소스의 상대 경로 또는 게임에서 저장해야 하는 기타 데이터를 저장합니다.
4. 새 가격을 추가하고 이전 단계에서 만든 화폐를 선택합니다. Amount를 기본적으로 설정하고 싶은 가격으로 설정합니다. 나중에 만드는 모든 스토어에서 가격을 재정의할 수 있습니다.
5. **Save and publish**를 선택하여 변경 사항을 완료합니다.
6. **Items** 목록에서 아이템을 확인합니다.
7. 마지막으로, 새 Economy Catalog (V2) Store를 만듭니다.
8. **Title**을 편집하고 **Description**을 추가합니다. 예를 들어, `Villager Store`, `A humble store run by a humble villager.`
9. 검색이 쉽도록 **Friendly ID**를 지정합니다. 예를 들어, `villagerstore`.
10. 이전 단계에서 만든 아이템을 스토어에 추가합니다. 여러 아이템을 스토어에 추가할 수 있으며 필요한 경우 기본 가격을 재정의할 수 있습니다.
11. **Save and publish**를 선택하여 변경 사항을 완료합니다.
12. **Stores** 목록에서 스토어를 확인합니다.

### REST API

***

PlayFab 타이틀의 설정이 완료되었습니다.

## 테스트

테스트 목적으로 Alpha/Beta 릴리스를 사용해 앱을 다운로드합니다.

* 테스트 계정과 실제 Android 기기를 사용해야 합니다.
* 앱을 시작하면 IAP가 초기화되고 아이템을 나타내는 \_버튼 하나\_가 표시됩니다.
* 해당 버튼을 선택합니다.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-test-app-iap.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=0570ae5b0f89411e4b2fe56fe1164bab" alt="테스트 앱 - 100 Diamonds 구매 버튼" width="1082" height="468" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-test-app-iap.png" />

IAP 구매가 시작됩니다. 구매가 성공할 때까지 Google Play 지시를 따르세요.

<img src="https://mintcdn.com/microsoft-4404708b/Oa_DP1q6U1lat7c2/images/playfab/economy-monetization/economy-v2/tutorials/unity-android-test-app-google-play-payment-successful.png?fit=max&auto=format&n=Oa_DP1q6U1lat7c2&q=85&s=281c211a7ceb93300685c9c9e593cc8e" alt="테스트 앱 - Google Play - 결제 성공" width="428" height="290" data-path="images/playfab/economy-monetization/economy-v2/tutorials/unity-android-test-app-google-play-payment-successful.png" />

마지막으로 PlayFab **Game Manager** 대시보드에서 타이틀로 이동하여 **New Events**를 찾습니다.

구매가 제공되고 검증되어 PlayFab 생태계로 전달되었는지 확인합니다.

UnityIAP와 Android Billing API를 PlayFab 애플리케이션에 성공적으로 통합했습니다!

## 다음 단계

1. 데모 IMGUI 표시를 대체할 구매용 Unity UI Toolkit 인터페이스를 구축합니다.
2. PlayFab 오류를 처리하고 사용자에게 표시하기 위한 사용자 지정 Unity Logger를 만듭니다.
3. Unity UI에 표시할 아이콘 이미지를 PlayFab Items Images 필드에 추가합니다.


## Related topics

- [PlayFab Economy 레거시, Unity IAP, Android 시작하기](/ko/services/playfab/economy-monetization/economy/tutorials/getting-started-with-unity-iap-android.md)
- [Android용 푸시 알림](/ko/services/playfab/live-service-management/game-configuration/title-communications/push-notifications/push-notifications-for-android.md)
- [Google Marketplace 리뎀션](/ko/services/playfab/economy-monetization/economy-v2/marketplace/marketplace-redemption/google.md)
- [PlayFab UGC 문서](/ko/services/playfab/economy-monetization/economy-v2/ugc/index.md)
- [PlayFab Economy v2 문서](/ko/services/playfab/economy-monetization/index.md)
