# Unity 게임에 웹숍 통합

> 인증된 플레이어 세션이 있는 Unity 게임에서 웹숍을 엽니다.

플레이어가 인앱 구매(IAP) 카탈로그에서 상품을 구매할 수 있도록 Unity 게임에서 웹숍을 엽니다.

웹숍은 이미 설정한 IAP 카탈로그 및 결제 제공업체를 사용합니다. 플레이어는 별도의 게임 내 스토어프론트 없이 웹에서 구매를 완료할 수 있습니다.

스토어를 열려면 플레이어의 인증된 세션이 첨부된 상태로 스토어 URL을 여는 버튼과 같은 게임 내 액션을 추가합니다. 세션은 플레이어를 웹숍으로 식별합니다.

## 필수 조건##prerequisites

시작하기 전에, 다음 필수 조건을 충족해야 합니다.

* [인앱 구매](/iap.md)를 통합하는 Unity 프로젝트입니다.
* 프로젝트의 [Authentication](/authentication.md) SDK(버전 3.7.1 이상)
* 타겟 환경의 웹숍입니다.

라이브 스토어를 열려면 웹숍을 게시하여 `shop.unity.com/{studio}/game/{slug}`에서 사용할 수 있도록 합니다. 드래프트를 테스트하려면 타겟 환경에서 생성된 웹숍만 필요합니다.

자세한 내용은 [첫 번째 웹숍 만들기 및 게시](./create-and-publish-your-first-webshop.md)를 참조하십시오.

## 게임에서 웹숍 열기##open-the-webshop-from-your-game

### SDK 헬퍼 사용##use-the-sdk-helper

Unity IAP SDK는 필요한 매개 변수와 규정 준수 검사를 통해 웹숍을 열 수 있는 `RedirectToWebshop`를 제공합니다.

결제 제공업체의 `IPaymentProvidersExtendedPurchaseService`를 통해 `RedirectToWebshop`에 전화하십시오. `catalogListingId`를 비워 두면 웹숍 앞 페이지를 열거나 제품 페이지를 직접 열려면 리스팅 ID를 전달합니다.

```csharp
// Open the front page
 UnityIAPServices.StoreController(PaymentProvider.Name).PaymentProvidersExtendedPurchaseService.RedirectToWebshop();

// Open a specific product page
 UnityIAPServices.StoreController(PaymentProvider.Name).PaymentProvidersExtendedPurchaseService.RedirectToWebshop(catalogListingId: "your-listing-id");
```

#### 열기 전에 규정 준수 승인 필요##require-compliance-approval-before-opening

규정 준수 검사 시 웹숍을 게이트하려면 `RedirectToWebshop`를 호출하기 전에 `SetComplianceCheck`에 콜백을 등록하십시오. 콜백은 각 리디렉션 전에 실행됩니다. `false`를 반환하면 리디렉션이 취소되고 `PurchasingUnavailable`로 구매가 실패합니다.

```csharp
StoreController(PaymentProvider.Name).PaymentProvidersExtendedPurchaseService
    .SetComplianceCheck(async context => await ShowComplianceDialog(context));
```

#### 웹숍 열기 방법 제어##control-how-the-webshop-opens

기본적으로 SDK는 외부 브라우저에서 웹숍을 엽니다. 변경하려면 리디렉션하기 전에 결제 제공업체의 `IPaymentProvidersExtendedPurchaseService`에서 `SetWebshopPresentationMode(CheckoutPresentationMode)`에 문의하십시오. 이 설정은 `SetCheckoutPresentationMode`와는 독립적입니다.

### 수동 연동##manual-integration

SDK 헬퍼를 사용할 수 없는 경우 웹숍 URL을 직접 구성하고 엽니다.

버튼 또는 기타 게임 내 액션으로 웹숍을 열려면 Webshop 서비스에서 쇼핑 URL을 확인한 다음 URL을 `Application.OpenURL`로 엽니다.

런타임 시 URL을 확인하면 동일한 빌드가 퍼블리시된 스토어나 환경 드래프트 미리보기를 엽니다. 이를 통해 퍼블리시하기 전에 드래프트를 테스트할 수 있습니다.

URL을 확인하려면 `storefront-link` 엔드포인트로 `GET` 요청을 보냅니다.

```text
https://webshop.services.api.unity.com/v1/projects/{projectId}/environments/{environmentId}/storefront-link
```

기본적으로 서비스는 환경 상태에 따라 URL을 자동으로 반환합니다.

* 퍼블리시된 프로덕션 환경은 라이브 공개 스토어프론트 URL을 반환합니다. 라이브 URL은 익명으로 확인됩니다.
* 비프로덕션 환경 또는 퍼블리시되지 않은 프로덕션 환경은 수명이 짧은 초안 미리보기 URL을 반환합니다.

라이브 스토어에 게시되지 않은 변경 사항을 미리 보려면 `source=draft` 쿼리 파라미터를 추가합니다. 퍼블리시된 프로덕션 환경에도 초안 미리보기 URL을 반환합니다.

```text
https://webshop.services.api.unity.com/v1/projects/{projectId}/environments/{environmentId}/storefront-link?source=draft
```

통합은 다음 Authentication SDK 토큰을 사용합니다.

* 액세스 토큰은 초안 미리보기를 확인할 때 `storefront-link` 요청을 승인합니다. `Authorization: Bearer` 헤더에 액세스 토큰을 전송합니다.
* 세션 토큰은 브라우저에서 플레이어를 인증합니다. 스토어를 열기 직전에 단기간의 단일 사용 제한 토큰을 `GenerateRestrictedTokenAsync`로 채운 다음 해당 세션 토큰을 해결된 URL에 `sessionToken` 쿼리 파라미터로 추가합니다.

URL을 확인한 후 세션 토큰, 프로젝트 ID, 환경을 추가하여 올바른 플레이어에게 상점을 열 수 있습니다.

각 쿼리 파라미터와 파라미터가 누락되면 상점이 어떻게 작동하는지에 대한 자세한 내용은 [게임에서 웹숍 열기](./deep-links.md#outbound-links)를 참조하십시오.

```csharp
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Services.Authentication;
using UnityEngine;
using UnityEngine.Networking;

public class WebshopLauncher : MonoBehaviour
{
    // Replace these with the values from your Unity Cloud project and webshop configuration.
    const string ProjectId       = "<your-project-id>";
    const 문자열 EnvironmentId = "<your-environment-id>"; // API 요청 경로에서 사용
    const 문자열 EnvironmentName = "production"; // 스토어 URL에 사용됨

    const 문자열 StorefrontLinkEndpoint =
        "https://webshop.services.api.unity.com/v1/projects/{0}/environments/{1}/storefront-link";

    // draftPreview: request source=draft to preview unpublished changes even on a live env.
    public void OpenShop(string locale, string currency, bool draftPreview = false)
    {
        StartCoroutine(OpenShopRoutine(locale, currency, draftPreview));
    }

    IEnumerator OpenShopRoutine(문자열 로케일, 문자열 통화, bool draftPreview)
    {
        var signedIn = AuthenticationService.Instance.IsSignedIn;

        // The access token is required for draft previews; live storefronts open anonymously
        // and ignore any token sent. 플레이어가 로그인할 때마다 전송합니다.
        if (draftPreview && !signedIn)
        {
            Debug.LogWarning("WebshopLauncher: draft preview requires the player to be signed in.");
            yield break;
        }

        // 1. Webshop 서비스에 스토어프론트 URL을 묻습니다.
        var endpoint = string.Format(StorefrontLinkEndpoint, ProjectId, EnvironmentId);
        if (draftPreview)
            엔드포인트 += "?source=draft";

        using var request = UnityWebRequest.Get(endpoint);
        request.SetRequestHeader("Accept", "application/json");
        if (signedIn)
            request.SetRequestHeader("Authorization", $"Bearer {AuthenticationService.Instance.AccessToken}");

        yield return request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"WebshopLauncher: storefront-link request failed " +
                           $"({request.responseCode}): {request.error}");
            yield break;
        }

        var link = JsonUtility.FromJson<StorefrontLinkResponse>(request.downloadHandler.text);
        if (link == null || string.IsNullOrEmpty(link.storefrontUrl))
        {
            Debug.LogError("WebshopLauncher: storefront-link response did not contain a storefrontUrl.");
            yield break;
        }

        // 2. 웹숍 리디렉션을 위한 수명이 짧고 일회용으로 제한된 토큰을 사용합니다.
        var tokenOptions = new RestrictedTokenOptions
        {
            서비스 = 새로운 목록<string> { "no-svc" }, // 실제 서비스에 대해 ID 토큰을 사용할 수 없음
            SingleUse = true, // 첫 새로고침 시점에 웹숍에서 사용됩니다.
            TtlSeconds = 60, // 리디렉트 직전에 틴트됩니다.
        };

        var tokenTask = AuthenticationService.Instance.GenerateRestrictedTokenAsync(tokenOptions);
        yield return new WaitUntil(() => tokenTask.IsCompleted);

        if (tokenTask.IsFaulted)
        {
            Debug.LogError($"WebshopLauncher: failed to generate restricted token: {tokenTask.Exception}");
            yield break;
        }

        // 3. 상점을 엽니다.
        var sessionToken = tokenTask.Result.SessionToken;
        var shopUrl = BuildShopUrl(link.storefrontUrl, sessionToken, locale, currency);

        Application.OpenURL(shopUrl);
    }

    정적 문자열 BuildShopUrl(문자열 storefrontUrl, 문자열 sessionToken, 문자열 로케일, 문자열 통화)
    {
        var url = storefrontUrl;
        url = AppendParam(url, "sessionToken", sessionToken);
        url = AppendParam(url, "projectId", ProjectId);
        url = AppendParam(url, "environment", EnvironmentName);
        url = AppendParam(url, "locale", locale);
        url = AppendParam(url, "currency", currency);
        return url;
    }

    정적 문자열 AppendParam(문자열 URL, 문자열 키, 문자열 값)
    {
        if (string.IsNullOrEmpty(value))
            return url;

        var separator = url.Contains("?") ? '&' : '?';
        return $"{url}{separator}{key}={UnityWebRequest.EscapeURL(value)}";
    }

    [직렬화 가능]
    StorefrontLinkResponse 클래스
    {
        public string storefrontUrl;
        public bool live;
    }
}
```

스토어가 열리면 URL에서 이러한 파라미터를 제거하고 브라우저에서 플레이어 세션을 유지합니다. 세션 및 지속성에 대한 자세한 내용은 [게임에서 웹숍 열기](./deep-links.md#authenticate-players-and-persist-sessions)를 참조하십시오.

> **Warning:**
>
> 아동 개인정보 아동 데이터 법률은 COPPA(아동 온라인 프라이버시 보호법)를 포함하되 이에 국한되지 않습니다. 연령 제한이 있는 사용자(예: 해당 법률에 따라 13세, 16세 또는 18세 미만의 어린이)로부터 데이터를 수집하고 사용하는 방법을 제한합니다. 연령 제한이 있는 사용자의 "개인 정보"는 [Unity 서비스 약관](https://unity.com/legal/terms-of-service)에 명시된 해당 법률을 준수하지 않는 한 Unity에 전송하지 않습니다.

전달할 로케일이나 재화가 없으면 해당 파라미터를 생략합니다. 기본적으로 IAP 카탈로그는 플레이어의 브라우저 로케일을 사용하며 기본값은 미국 달러(USD)입니다. 카탈로그 로케일 처리에 대한 자세한 내용은 [카탈로그 및 웹숍 결제](./catalog-and-payments.md)를 참조하십시오.

## 들어오는 딥 링크 처리##handle-inbound-deep-links

상점에 대한 **Deeplink URL**을 설정하면 해당 사용자 지정 URL 체계를 통해 플레이어가 게임으로 돌아옵니다. 구매 후 플레이어가 인증되지 않은 시작 페이지에서 **Connect to game**을 선택할 때 상점은 반환 딥 링크를 사용합니다.

반환 딥 링크를 받으려면 기기에 커스텀 URL 체계를 등록하고 런타임 시 들어오는 링크를 처리합니다. 상점에서 반환 URL을 빌드하는 방법에 대한 자세한 내용은 [게임에서 상점 열기](./deep-links.md#post-purchase-return-to-the-game)를 참조하십시오.

> **Note:**
>
> **Connect to game** 플로에서 Unity IAP SDK는 자동으로 웹숍을 다시 엽니다. 이 링크를 직접 처리할 필요는 없습니다.
>
> 다음 요구 사항을 충족해야 합니다.
>
> * 플레이어가 IAP를 초기화하기 전에 Unity Authentication에 로그인합니다.
> * PaymentProvider 스토어가 연결되었습니다.
>
> 후 구매 수익은 표준 플랫폼 커스텀 URL 체계 처리를 사용하며 Unity 전용 SDK가 필요하지 않습니다.

> **Note:**
>
> 딥 링크는 Unity 에디터 WebGL이 아닌 빌드된 플레이어에서만 확인됩니다. 에디터는 커스텀 체계의 핸들러로 등록되지 않으므로 브라우저에서 열린 링크는 플레이 모드에 도달하지 않으며, WebGL 빌드는 커스텀 체계가 아닌 페이지 URL을 사용합니다. 기기(iOS 또는 Android) 또는 스탠드얼론 빌드에서 반환 플로를 테스트합니다. 플랫폼 지원 및 등록에 대한 자세한 내용은 Unity의 [세부 링크](https://docs.unity3d.com/Manual/deep-linking.html) 매뉴얼을 참조하십시오.

### URL 체계 등록##register-the-url-scheme

대시보드의 **Deeplink URL** 필드에서 설정한 것과 동일한 체계를 선언합니다. 운영체제는 이 스키마를 사용하여 게임에 대한 링크를 라우팅합니다.

* **iOS 및 macOS**: **Edit > Project Settings > Player > Other Settings > Supported URL schemes**에서 스키마를 추가합니다. Unity는 빌드 시 빌드된 앱의 `Info.plist`(`CFBundleURLTypes`)에 기록합니다. 모든 빌드에서 재생되는 생성된 `Info.plist`를 수동으로 편집하는 것보다 이것을 선호합니다.
* **Android**: 사용자 지정 메인 매니페스트 또는 Gradle 매니페스트 템플릿을 통해 활동에 `<data android:scheme="mygame" />` 항목이 있는 `intent-filter`를 추가합니다.

스키마는 **Deeplink URL** 값과 정확히 일치해야 합니다. Android 매니페스트를 변경한 후 운영체제에서 새 체계를 선택할 수 있도록 게임을 다시 설치합니다.

### 런타임 시점에 링크 처리##handle-the-link-at-runtime

게임 실행 중에 도착하는 링크를 `Application.deepLinkActivated`에 구독하십시오. 시작 시 딥 링크가 게임을 시작하는 콜드 스타트를 처리하도록 `Application.absoluteURL`를 확인합니다.

```csharp
void Awake()
{
    // Links that arrive while the game is running.
    Application.deepLinkActivated += OnReturnFromWebshop;

    // Cold start: the deep link launched the game.
    if (!string.IsNullOrEmpty(Application.absoluteURL))
        OnReturnFromWebshop(Application.absoluteURL);
}

void OnReturnFromWebshop(string url)
{
    // Handle the post-purchase return: the shop appends ?status=success
    // (and playerId when available) after a completed purchase.
    if (new Uri(url).Query.Contains("status=success"))
    {
        // Purchase completed on the web — refresh the player's entitlements.
    }
    // The Connect to game sign-in link is reopened by the SDK automatically,
    // so it needs no handling here.
}
```

이 가게는 `status=success`만 보냅니다, 그래서 여기서 그 케이스를 처리하십시오. SDK는 이전 참고 사항에 설명된 대로 자동으로 **게임 연결** 로그인 링크를 다시 엽니다.

## 상점 열기 테스트##test-opening-the-shop

퍼블리시하기 전에 프래프트 미리보기를 테스트한 다음 라이브 스토어에서 테스트를 반복합니다. 초안을 테스트하려면 `draftPreview: true`와 함께 `OpenShop`에 전화하여 플레이어가 로그인되었는지 확인합니다. 비프로덕션 환경을 타게팅하거나 `source=draft`를 사용하여 프로덕션 환경의 퍼블리시되지 않은 변경 사항을 미리 볼 수 있습니다.

통합을 확인하려면 다음 단계를 따르십시오.

1. 모바일 디바이스에서 게임을 빌드하고 설치합니다.
2. 장치의 시스템 브라우저를 실행하도록 `OpenShop`를 호출하는 게임 내 버튼을 트리거합니다.
3. 브라우저가 URL에 로케일과 통화를 사용하여 확인된 스토어프론트 URL로 열립니다. 초안은 환경 범위의 미리보기 URL을 엽니다. 라이브 스토어가 `shop.unity.com/{studio}/game/{slug}`를 열립니다.
4. 스토어가 인증되지 않은 랜딩 페이지가 아닌 인증된 제품 목록으로 열립니다.
5. 샌드박스 구매를 완료합니다. 테스트 자격 증명은 관련 IAP 결제 제공업체의 샌드박스 기술 자료를 참고하십시오.
6. 초안이 예상대로 작동하면 `draftPreview: false`에 `OpenShop`를 호출하고 실시간으로 게시된 스토어에 대해 테스트를 반복합니다.

제품 목록 대신 인증되지 않은 시작 페이지로 가면 게임이 URL에 유효한 `sessionToken` 및 `projectId`를 전달했는지 확인합니다. [문제 해결 웹숍](./troubleshooting.md)을 참조하십시오.
