Unclaimed Package Is this your package? Claim it to unlock full analytics and manage your listing.
Claim This Package

Install via UPM

Add to Unity Package Manager using this URL

https://www.pkglnk.dev/ait-sdk.git?path=webapp

README Markdown

Copy this to your project's README.md

Style
Preview
pkglnk installs badge
## Installation

Add **AppInToss SDK** to your Unity project via Package Manager:

1. Open **Window > Package Manager**
2. Click **+** > **Add package from git URL**
3. Enter:
```
https://www.pkglnk.dev/ait-sdk.git?path=webapp
```

[![pkglnk](https://www.pkglnk.dev/badge/ait-sdk.svg?style=pkglnk)](https://www.pkglnk.dev/pkg/ait-sdk)

README

AIT Unity SDK (Toss ์—ฐ๋™ ํ…œํ”Œ๋ฆฟ)

GitHub Actions Status

๐Ÿ“Œ ๊ฐœ์š”

์ด ํ”„๋กœ์ ํŠธ๋Š” Unity WebGL ๋นŒ๋“œ๋ฅผ ํ† ์Šค ์•ฑ ๋‚ด ์›น๋ทฐ์—์„œ ์‹คํ–‰๋˜๋Š” React ์›น์•ฑ์— ํ†ตํ•ฉํ•˜๊ธฐ ์œ„ํ•œ ํ…œํ”Œ๋ฆฟ ๋ฐ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. app-webview-rpc๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Protobuf๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Unity(C#)์™€ React(TypeScript) ๊ฐ„์˜ ํƒ€์ž…-์„ธ์ดํ”„(type-safe)ํ•œ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.

์ด ์ €์žฅ์†Œ๋Š” ๋ณ„๋„์˜ npm ํŒจํ‚ค์ง€๋กœ ์ œ๊ณต๋  ๊ณ„ํš์€ ์—†์œผ๋ฉฐ, ์ „์ฒด๋ฅผ ํด๋ก ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ์ฐธ๊ณ ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

โœจ ์ฃผ์š” ๊ธฐ์ˆ 

  • Unity (C#): ๊ฒŒ์ž„ ํด๋ผ์ด์–ธํŠธ
  • React (TypeScript): ์›น ํ”„๋ก ํŠธ์—”๋“œ ๋ฐ ํ† ์Šค ์•ฑ ๋ธŒ๋ฆฟ์ง€ ์—ฐ๋™
  • Protobuf: Unity-React ๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ ์Šคํ‚ค๋งˆ ์ •์˜
  • WebView-RPC: Protobuf ๊ธฐ๋ฐ˜์˜ RPC ํ”„๋ ˆ์ž„์›Œํฌ
  • GitHub Actions: Protobuf ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ C# ๋ฐ TypeScript ์ฝ”๋“œ๋ฅผ ์ž๋™ ์ƒ์„ฑ

๐Ÿš€ ์‹œ์ž‘ํ•˜๊ธฐ

1. ์›น์•ฑ ์„ค์ • (React)

  1. ํ”„๋กœ์ ํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ ์ด ์ €์žฅ์†Œ ์ „์ฒด๋ฅผ ํด๋ก ํ•˜๊ฑฐ๋‚˜, webapp ์„œ๋ธŒ ํด๋”๋งŒ ๋ณต์‚ฌํ•˜์—ฌ ๊ธฐ์กด React ํ”„๋กœ์ ํŠธ์— ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    # webapp ํด๋”๋กœ ์ด๋™
    cd webapp
    # ์˜์กด์„ฑ ์„ค์น˜
    npm install
    
  2. ์„ค์ • ํŒŒ์ผ ์ˆ˜์ • webapp/granite.config.ts ํŒŒ์ผ์„ ์—ด์–ด ์ž์‹ ์˜ ํ† ์Šค ์•ฑ ์„ค์ •์— ๋งž๊ฒŒ appId, displayName ๋“ฑ์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

  3. [์„ ํƒ] UI ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ํ•„์š”์— ๋”ฐ๋ผ webapp/src/App.tsx ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์—ฌ ์›ํ•˜๋Š” UI์™€ ๊ธฐ๋Šฅ์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  4. Unity ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ ์—ฐ๋™

    • Unity์—์„œ WebGL ๋นŒ๋“œ๋ฅผ ์™„๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
    • ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ 4๊ฐœ (.data, .framework, .loader, .wasm) ํŒŒ์ผ์„ webapp/public/assets/ ํด๋”์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
    • webapp/src/App.tsx ํŒŒ์ผ ๋‚ด์—์„œ Unity ๋นŒ๋“œ ํŒŒ์ผ๋ช…์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ง€์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ๋‹ค๋ฅผ ๊ฒฝ์šฐ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

2. Unity SDK ์„ค์ •

  1. NuGetForUnity ์„ค์น˜ Unity ํ”„๋กœ์ ํŠธ์—์„œ GlitchEnzo/NuGetForUnity๋ฅผ Unity Package Manager๋ฅผ ํ†ตํ•ด ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

    • Package Manager > "Add package from git URL..." ์„ ํƒ
    • https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src ์ž…๋ ฅ
  2. Google.Protobuf ์„ค์น˜

    • NuGetForUnity ์„ค์น˜ ํ›„ ์ƒ๋‹จ ๋ฉ”๋‰ด NuGet > Manage NuGet Packages๋ฅผ ์—ฝ๋‹ˆ๋‹ค.
    • Google.Protobuf๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์ตœ์‹  ๋ฒ„์ „์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.
  3. AIT-SDK ํŒจํ‚ค์ง€ ์„ค์น˜

    • OpenUPM-CLI๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (npm install -g openupm-cli)
    • ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. (์˜์กด์„ฑ์„ ์ž๋™์œผ๋กœ ํ•ด๊ฒฐํ•ด์ค๋‹ˆ๋‹ค.)
    openupm add com.kwanjoong.ait-sdk
    
  4. AitSdkBridge ์„ค์ •

    • Unity์˜ ์‹œ์ž‘ ์”ฌ(Scene)์˜ ์ตœ์ƒ์œ„(root)์— AitSdkBridge๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋นˆ ๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. (์ด๋ฆ„์ด ์ •ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.)
    • ์ƒ์„ฑ๋œ ๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ์— AitRpcBridge ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ€์ฐฉํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ์˜ค๋ธŒ์ ํŠธ๋Š” ๊ฒŒ์ž„ ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ DontDestroyOnLoad๋กœ ์ „ํ™˜๋˜์–ด ๊ฒŒ์ž„ ์„ธ์…˜ ๋™์•ˆ RPC ํ†ต์‹ ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“š SDK ์‚ฌ์šฉ๋ฒ•

๋ชจ๋“  ๊ธฐ๋Šฅ์€ AitRpcBridge ์‹ฑ๊ธ€ํ†ค์„ ํ†ตํ•ด ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.

// AitRpcBridge๋Š” ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
var ads = AitRpcBridge.Instance.Ads;   // ๊ณ ์ˆ˜์ค€ ๊ด‘๊ณ  ์œ ์ฆˆ์ผ€์ด์Šค
var iaps = AitRpcBridge.Instance.Iaps; // ๊ณ ์ˆ˜์ค€ IAP ์œ ์ฆˆ์ผ€์ด์Šค
var storageService = AitRpcBridge.Instance.StorageService; // ์ €์ˆ˜์ค€ RPC ํด๋ผ์ด์–ธํŠธ (ํ•„์š” ์‹œ)

๋ ˆ์ด์–ด ์•ˆ๋‚ด

  1. UseCase Layer (๊ถŒ์žฅ) โ€“ Ads, Iaps์ฒ˜๋Ÿผ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฐ”๋กœ ํ˜ธ์ถœํ•  ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค. ํ† ์Šค ์ •์ฑ… ์ค€์ˆ˜ ๋กœ์ง์ด ํฌํ•จ๋œ ๋ชจ๋ฒ” ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.
  2. Generated RPC Layer โ€“ AdServiceClient, IapServiceClient ๋“ฑ ์ž๋™ ์ƒ์„ฑ๋œ Stub. ํŠน์ˆ˜ํ•œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ด ํ•„์š”ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”.
  3. Extension Methods โ€“ RPC ํ˜ธ์ถœ ํŒจํ„ด์„ ๋‹จ์ˆœํ™”ํ•œ Helper (ShowAdAsStream ๋“ฑ). ๊ณ ๊ธ‰ ์„น์…˜์—์„œ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

DeviceService (๊ธฐ๊ธฐ ์ •๋ณด)

Safe Area (์•ˆ์ „ ์˜์—ญ) ์ ์šฉ

SafeAreaManager๊ฐ€ ๊ฒŒ์ž„ ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ ๊ธฐ๊ธฐ์˜ Safe Area ๊ฐ’์„ ๊ฐ€์ ธ์™€ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ UI์— ์ ์šฉํ•˜๋ ค๋ฉด, Safe Area๋ฅผ ์ ์šฉํ•  UI Panel ๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ์— SafeAreaPanel ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ€์ฐฉํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  1. ๋งค๋‹ˆ์ € ์„ค์ •: ์‹œ์ž‘ ์”ฌ์˜ AitSdkBridge ๋˜๋Š” ๋‹ค๋ฅธ ๋งค๋‹ˆ์ € ์˜ค๋ธŒ์ ํŠธ์— SafeAreaManager ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ€์ฐฉํ•ฉ๋‹ˆ๋‹ค.
  2. ํŒจ๋„ ์ ์šฉ: ์•ˆ์ „ ์˜์—ญ์„ ์ ์šฉํ•  ๋ชจ๋“  UI Panel์— SafeAreaPanel ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ€์ฐฉํ•ฉ๋‹ˆ๋‹ค.

StorageService (์˜๊ตฌ ์ €์žฅ์†Œ)

ํ† ์Šค ์•ฑ ๋‚ด์—์„œ Key-Value ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.

using AitBridge.RPC;
using AIT.AIT_SDK.ExtensionMethods; // ํ™•์žฅ ๋ฉ”์„œ๋“œ using ํ•„์ˆ˜!
using Cysharp.Threading.Tasks;

// ๋ฐ์ดํ„ฐ ์ €์žฅ
await AitRpcBridge.Instance.StorageService.SetItem(new () { Key = "BestScore", Value = "100" });

// ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
var response = await AitRpcBridge.Instance.StorageService.GetItem(new () { Key = "BestScore" });
string score = response.Value; // "100"

// ๋ฐ์ดํ„ฐ ์‚ญ์ œ
await AitRpcBridge.Instance.StorageService.RemoveItem(new () { Key = "BestScore" });

// ์ „์ฒด ๋ฐ์ดํ„ฐ ์‚ญ์ œ
await AitRpcBridge.Instance.StorageService.ClearItems();

IAP (AppsInTossIapUseCase)

AitRpcBridge.Instance.Iaps๋ฅผ ํ†ตํ•ด ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ๋ชจํŠธ ์นดํƒˆ๋กœ๊ทธ, ์ปค์Šคํ…€ Spot ์ •๋ณด, ๊ฒฐ์ œ ํ”Œ๋กœ์šฐ๋ฅผ ํ•œ ๊ณณ์—์„œ ์ฑ…์ž„์ง‘๋‹ˆ๋‹ค.

using AIT.AIT_SDK.Bridge;
using Cysharp.Threading.Tasks;

public class ShopPanel : MonoBehaviour
{
    public async UniTask InitializeAsync()
    {
        // 1) Toss ๋ฆฌ๋ชจํŠธ ์นดํƒˆ๋กœ๊ทธ๋กœ ์ƒ์  ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง
        var catalog = await AitRpcBridge.Instance.Iaps.GetRemoteCatalogAsync();
        RenderStore(catalog);

        // 2) ํŠน์ • Spot(์˜ˆ: remove_ads_button) ๋…ธ์ถœ
        var curated = await AitRpcBridge.Instance.Iaps.GetCuratedProductAsync("remove_ads_button");
        if (curated != null)
        {
            RenderFeaturedTile(curated.Value);
        }
    }

    public async UniTask PurchaseRemoveAdsAsync()
    {
        var result = await AitRpcBridge.Instance.Iaps.PurchaseCuratedSpotAsync("remove_ads_button");
        if (result.IsSuccess)
        {
            UnlockRemoveAds();
        }
        else
        {
            ShowPurchaseError(result.ErrorEvent?.ErrorMessage);
        }
    }
}

UseCase๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ Toss GetProductItemList / CreateOneTimePurchaseOrder / PollPurchaseEvents๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ScriptableObject(AppsInTossMonetizationConfig.asset)์— ๋“ฑ๋ก๋œ SKU ์ •๋ณด๋ฅผ ์ฐธ๊ณ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ง ๊ณ ๊ธ‰: ๋กœ์šฐ ๋ ˆ๋ฒจ RPC ์ง์ ‘ ์‚ฌ์šฉ
using Ait.Iap;
using AitBridge.RPC;
using AIT.AIT_SDK.ExtensionMethods;
using Cysharp.Threading.Tasks;

public class IapTest : MonoBehaviour
{
    public async UniTask TestPurchase(string sku)
    {
        var request = new CreateOneTimePurchaseOrderRequest { Sku = sku };

        try
        {
            await foreach (var ev in AitRpcBridge.Instance.IapService.CreateOrderAsStream(request))
            {
                switch (ev.EventCase)
                {
                    case PurchaseEvent.EventOneofCase.Success:
                        Debug.Log($"Purchase Success! Order ID: {ev.Success.OrderId}");
                        break;
                    case PurchaseEvent.EventOneofCase.Error:
                        Debug.LogError($"Purchase Error! Code: {ev.Error.ErrorCode}, Msg: {ev.Error.ErrorMessage}");
                        break;
                }
            }
            Debug.Log("Purchase flow finished.");
        }
        catch (Exception e)
        {
            Debug.LogError($"Purchase stream failed: {e.Message}");
        }
    }
}

Ads (AppsInTossAdUseCase)

AitRpcBridge.Instance.Ads๊ฐ€ ๊ด‘๊ณ  ํ˜ธ์ถœ๊ณผ Toss ๊ทœ์ • ์ค€์ˆ˜๋ฅผ ๋ชจ๋‘ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

using AIT.AIT_SDK.Bridge;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class RewardedButton : MonoBehaviour
{
    public async void OnClick()
    {
        var result = await AitRpcBridge.Instance.Ads.ShowRewardedAsync();
        if (result.IsRewardGranted)
        {
            GrantReward();
        }
        else if (result.Status == AppsInTossAdStatus.FailedToShow)
        {
            ShowRetryPopup();
        }
    }
}

๊ด‘๊ณ  ์žฌ์ƒ ์ค‘ Time.timeScale/AudioListener.pause๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€๋Š” AppsInTossMonetizationConfig์˜ ํ† ๊ธ€์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค. Toss WebView๊ฐ€ ์ˆจ๊ฒจ์กŒ์„ ๋•Œ์˜ ์ฒ˜๋ฆฌ ์—ญ์‹œ ๊ฐ™์€ ์„ค์ •์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

๐Ÿ”ง ๊ณ ๊ธ‰: ๋กœ์šฐ ๋ ˆ๋ฒจ RPC ์ง์ ‘ ์‚ฌ์šฉ
using Ait.Ad;
using AitBridge.RPC;
using AIT.AIT_SDK.ExtensionMethods;
using Cysharp.Threading.Tasks;

public class AdTest : MonoBehaviour
{
    public async UniTask TestShowAd(string adGroupId)
    {
        var request = new ShowAdRequest { AdGroupId = adGroupId };

        try
        {
            await foreach (var ev in AitRpcBridge.Instance.AdService.ShowAdAsStream(request))
            {
                switch (ev.EventCase)
                {
                    case ShowAdEvent.EventOneofCase.UserEarnedReward:
                        Debug.Log($"User Earned Reward! Type: {ev.UserEarnedReward.UnitType}, Amount: {ev.UserEarnedReward.UnitAmount}");
                        break;
                    case ShowAdEvent.EventOneofCase.Dismissed:
                        Debug.Log("Ad was dismissed.");
                        break;
                    case ShowAdEvent.EventOneofCase.FailedToShow:
                        Debug.Log("Ad failed to show.");
                        break;
                }
            }
            Debug.Log("Ad flow finished.");
        }
        catch (Exception e)
        {
            Debug.LogError($"ShowAdAsStream failed: {e.Message}");
        }
    }
}

๐Ÿ’ฐ ์ˆ˜์ตํ™” ๊ตฌ์„ฑ (Ads & IAP)

1. AppsInTossMonetizationConfig (ScriptableObject)

Assets/AppsInTossMonetizationConfig.asset์€ ๊ด‘๊ณ  ๊ทธ๋ฃน ID์™€ ์ปค์Šคํ…€ IAP ๋…ธ์ถœ์„ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์ค‘์•™ ์„ค์ •์ž…๋‹ˆ๋‹ค.

  • Ad Group IDs
    • Interstitial Ad Group Id: ๋ชจ๋“  ์ค‘๊ฐ„ ๊ด‘๊ณ ๊ฐ€ ์‚ฌ์šฉํ•  Toss adGroupId.
    • Rewarded Ad Group Id: ๋ชจ๋“  ๋ณด์ƒํ˜• ๊ด‘๊ณ ์˜ adGroupId.
  • Curated IAP Spots (Spot ๋ฆฌ์ŠคํŠธ๋Š” ์›ํ•˜๋Š” ๋งŒํผ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ)
    • Spot ID: ๊ฒŒ์ž„ ์ฝ”๋“œ๊ฐ€ ์ฐธ์กฐํ•˜๋Š” ๋กœ์ปฌ ์‹๋ณ„์ž (remove_ads_button, gem_shop_featured ๋“ฑ).
    • Product ID: Toss ๋Œ€์‹œ๋ณด๋“œ์—์„œ ๋ฐœ๊ธ‰ ๋ฐ›์€ SKU. ๊ฒฐ์ œ ์š”์ฒญ ์‹œ ์ด ๊ฐ’์ด ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.
    • Icon: Toss์—์„œ ๋‚ด๋ ค์ฃผ๋Š” ์ด๋ฏธ์ง€ ๋Œ€์‹  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์“ฐ๊ณ  ์‹ถ์€ ์Šคํ”„๋ผ์ดํŠธ.
    • Title Override: ๋…ธ์ถœ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง• (๋ฏธ์ž…๋ ฅ ์‹œ Toss display_name ์‚ฌ์šฉ).
    • Subtitle: ๋ณด์กฐ ์„ค๋ช…(โ€œ๊ด‘๊ณ  ์ œ๊ฑฐโ€, โ€œ๋ฒ ์ŠคํŠธ ๋ฐธ๋ฅ˜โ€ ๋“ฑ).
    • Call To Action Override: ๋ฒ„ํŠผ ๋ผ๋ฒจ ์ง€์ • (โ€œ๊ตฌ๋งคโ€, โ€œ์ถฉ์ „ํ•˜๊ธฐโ€ ๋“ฑ).
    • Highlight Color: ์นด๋“œ/๋ฑƒ์ง€์— ์‚ฌ์šฉํ•  ํฌ์ธํŠธ ์ƒ‰์ƒ.
  • Playback Behavior Toggles
    • Pause Time During Ads, Mute Audio During Ads: ๊ด‘๊ณ  ์žฌ์ƒ ๋™์•ˆ Time.timeScale๊ณผ ์˜ค๋””์˜ค๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ์„ ํƒ.
    • Pause Time When Host Hidden, Mute Audio When Host Hidden: ์‚ฌ์šฉ์ž๊ฐ€ ํ™ˆ ๋ฒ„ํŠผยท์ž ๊ธˆ ๋“ฑ์œผ๋กœ WebView ํ™”๋ฉด์„ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ์˜ ์ฒ˜๋ฆฌ.

์ฐธ๊ณ : ๊ด‘๊ณ /IAP์šฉ ์ €์ˆ˜์ค€ RPC ํด๋ผ์ด์–ธํŠธ(AdServiceClient, IapServiceClient)๋Š” ํŒจํ‚ค์ง€ ๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋ฉฐ, ๊ฒŒ์ž„ ์ฝ”๋“œ๋Š” ๋ฐ˜๋“œ์‹œ UseCase ๊ณ„์ธต์„ ํ†ตํ•ด ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.

2. AppsInTossAdUseCase (ํ†ตํ•ฉ ๊ด‘๊ณ  ์ง„์ž…์ )

AitRpcBridge.Instance.Ads๋ฅผ ํ†ตํ•ด ์–ธ์ œ๋“  ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ง์ ‘ AppsInTossAdUseCase.Instance๋ฅผ ํ˜ธ์ถœํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.)

var adResult = await AitRpcBridge.Instance.Ads.ShowRewardedAsync();
if (adResult.IsRewardGranted)
{
    GrantRewardToPlayer();
}
  • ShowInterstitialAsync, ShowRewardedAsync ๋‘ ๋ฉ”์„œ๋“œ๋งŒ ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • ์–ด๋–ค adGroupId๋ฅผ ์“ธ์ง€, ๊ด‘๊ณ  ์ค‘์— ์‹œ๊ฐ„์„ ๋ฉˆ์ถœ์ง€/์Œ์†Œ๊ฑฐํ• ์ง€๋Š” ScriptableObject ์„ค์ •์„ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.
  • ๋‚ด๋ถ€์ ์œผ๋กœ AppsInTossPlaybackPause๋ฅผ ์‚ฌ์šฉํ•ด Time.timeScale๊ณผ AudioListener.pause๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ฐธ์กฐ ์นด์šดํŠธ ๋ฐฉ์‹์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ, ๋‹ค๋ฅธ ์‹œ์Šคํ…œ๊ณผ ์ถฉ๋Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

3. AppsInTossIapUseCase (๋ฆฌ๋ชจํŠธ ์นดํƒˆ๋กœ๊ทธ + ์ปค์Šคํ…€ ๋…ธ์ถœ)

AitRpcBridge.Instance.Iaps๋ฅผ ํ†ตํ•ด ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.

// 1) ์ƒ์  ์ „์ฒด ๋ชฉ๋ก (๋ฆฌ๋ชจํŠธ ์นดํƒˆ๋กœ๊ทธ)
var catalog = await AitRpcBridge.Instance.Iaps.GetRemoteCatalogAsync();

// 2) ํŠน์ • Spot (์˜ˆ: remove_ads ๋ฒ„ํŠผ)
var curated = await AitRpcBridge.Instance.Iaps.GetCuratedProductAsync("remove_ads_button");
if (curated != null)
{
    RenderCustomCard(curated.Value);
}

// 3) ๊ตฌ๋งค
var result = await AitRpcBridge.Instance.Iaps.PurchaseCuratedSpotAsync("remove_ads_button");
  • ๋ฆฌ๋ชจํŠธ ์นดํƒˆ๋กœ๊ทธ: Toss์—์„œ ๋‚ด๋ ค์ฃผ๋Š” ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ/์ด๋ฏธ์ง€/๊ฐ€๊ฒฉ์„ ๊ทธ๋Œ€๋กœ UI์— ๋ฟŒ๋ฆด ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • Curated Spot: ํŠน์ • UI ์œ„์น˜(์˜ˆ: ํ™ˆ ํ™”๋ฉด ๊ด‘๊ณ  ์ œ๊ฑฐ ๋ฒ„ํŠผ)์— ๋Œ€ํ•ด, ํ˜„์ง€ํ™”ยท๊ฐ•์กฐ ์ƒ‰์ƒ ๋“ฑ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋ฉด์„œ๋„ ์‹ค์ œ ๊ฒฐ์ œ๋Š” Toss SKU์™€ 1:1๋กœ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.
  • ๊ฒฐ์ œ ํ๋ฆ„: PurchaseCuratedSpotAsync๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ฑ โ†’ ์ด๋ฒคํŠธ ํด๋ง โ†’ ์„ฑ๊ณต/์‹คํŒจ ๋ฐ˜ํ™˜๊นŒ์ง€ ์ „๋ถ€ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, PurchaseCompleted ์ด๋ฒคํŠธ๋กœ๋„ ๊ฒฐ๊ณผ๋ฅผ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. WebView ๊ฐ€์‹œ์„ฑ & ์ž๋™ ์žฌ์ƒ ์ œ์–ด

ํ† ์Šค ์ •์ฑ…์ƒ WebView๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด์ง€ ์•Š๋Š” ๋™์•ˆ์—๋Š” ๊ฒŒ์ž„์ด ์ž๋™์œผ๋กœ ๋ฉˆ์ถ”๊ณ  ์†Œ๋ฆฌ๊ฐ€ ๋‚˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด React/Unity ์–‘์ชฝ์— ํ›…์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

// webapp/src/App.tsx
useEffect(() => {
  const notify = (state: "hidden" | "visible") =>
    unityContext.sendMessage?.("AitRpcBridge", "OnHostVisibilityChanged", state);
  const handleVisibility = () => notify(document.hidden ? "hidden" : "visible");
  document.addEventListener("visibilitychange", handleVisibility);
  window.addEventListener("blur", () => notify("hidden"));
  window.addEventListener("focus", () => notify("visible"));
  handleVisibility(); // ์ดˆ๊ธฐ ์ƒํƒœ ์ „๋‹ฌ
  return () => {
    document.removeEventListener("visibilitychange", handleVisibility);
    window.removeEventListener("blur", () => notify("hidden"));
    window.removeEventListener("focus", () => notify("visible"));
  };
}, [unityContext]);

Unity์˜ AitRpcBridge.OnHostVisibilityChanged๋Š” ์œ„ ScriptableObject ์„ค์ •์— ๋”ฐ๋ผ AppsInTossPlaybackPause๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ Time.timeScale๊ณผ ์˜ค๋””์˜ค ์ƒํƒœ๋ฅผ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

5. Safe Area ๋ชจํ‚น

SafeAreaManager๋Š” WebGL (์‹ค ๋””๋ฐ”์ด์Šค)์—์„œ๋Š” Toss RPC๋กœ ์•ˆ์ „ ์˜์—ญ์„ ๋ฐ›๊ณ , Unity ์—๋””ํ„ฐ/Standalone์—์„œ๋Š” Screen.safeArea๋ฅผ ์ด์šฉํ•ด ์ธ์…‹์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ๋ณ„๋„์˜ ๋ชจํ‚น ์„ค์ • ์—†์ด๋„ ์—๋””ํ„ฐ์—์„œ UI๋ฅผ ๋งž์ถœ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ShareService (๊ณต์œ  ๋ฐ ๋ฆฌ์›Œ๋“œ)

๊ณต์œ ํ•˜๊ธฐ ๊ธฐ๋Šฅ์„ ํ˜ธ์ถœํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋‹จ์ผ ์‘๋‹ต์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.

using Ait.Share;
using AitBridge.RPC;
using Cysharp.Threading.Tasks;

public class ShareTest : MonoBehaviour
{
    public async UniTask TestShare(string moduleId)
    {
        var request = new ShowContactsViralRequest { ModuleId = moduleId };
        try
        {
            var response = await AitRpcBridge.Instance.ShareService.ShowContactsViral(request);
            
            switch (response.EventCase)
            {
                case ShowContactsViralResponse.EventOneofCase.Reward:
                    Debug.Log($"Share Reward! Amount: {response.Reward.RewardAmount}, Unit: {response.Reward.RewardUnit}");
                    break;
                case ShowContactsViralResponse.EventOneofCase.Close:
                    Debug.Log($"Share Closed! Reason: {response.Close.CloseReason}, Sent Count: {response.Close.SentRewardsCount}");
                    break;
                case ShowContactsViralResponse.EventOneofCase.Error:
                    Debug.LogError($"Share Error: {response.Error.Message}");
                    break;
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"ShowContactsViral RPC failed: {e.Message}");
        }
    }
}

๐Ÿ“ ์•„ํ‚คํ…์ฒ˜

์ด ํ”„๋กœ์ ํŠธ๋Š” Protobuf๋ฅผ ์ด์šฉํ•œ ์ฝ”๋“œ ์ƒ์„ฑ ๊ธฐ๋ฐ˜์˜ RPC ์•„ํ‚คํ…์ฒ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  1. proto/ ํด๋” ๋‚ด์˜ .proto ํŒŒ์ผ์— ์„œ๋น„์Šค์™€ ๋ฉ”์‹œ์ง€๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ Git์— Pushํ•˜๋ฉด, GitHub Action์ด .github/workflows/generate-protobuf.yml ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  3. ์›Œํฌํ”Œ๋กœ์šฐ๋Š” protoc-gen-webviewrpc ์ฝ”๋“œ ์ƒ์„ฑ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Unity์šฉ C# ์ฝ”๋“œ์™€ React์šฉ TypeScript ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ , generated-code ๋ธŒ๋žœ์น˜์— ์ปค๋ฐ‹ํ•ฉ๋‹ˆ๋‹ค.
  4. ๊ฐœ๋ฐœ์ž๋Š” generated-code ๋ธŒ๋žœ์น˜์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž์‹ ์˜ ๊ฐœ๋ฐœ ๋ธŒ๋žœ์น˜๋กœ ๊ฐ€์ ธ์™€(pull ๋˜๋Š” cherry-pick) ๊ตฌํ˜„์„ ๊ณ„์†ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค ๊ธฐ์—ฌํ•˜๊ธฐ

ํ”„๋กœ์ ํŠธ์— ๊ธฐ์—ฌํ•˜๊ณ  ์‹ถ์œผ์‹œ๋‹ค๋ฉด, ์ด์Šˆ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ Pull Request๋ฅผ ๋ณด๋‚ด์ฃผ์„ธ์š”.

๐Ÿ“œ ๋ผ์ด์„ ์Šค

์ด ํ”„๋กœ์ ํŠธ๋Š” MIT License๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

Comments

No comments yet. Be the first!