# 자체 관리형 재화

> 보상 콜백을 구성하고 서버에서 거래를 처리하여 자체 관리형 가상 재화로 탭조이 오퍼월을 설정합니다.

체 관리형 재화를 사용하면 사용자의 가상 재화를 자체 서버에서 직접 관리할 수 있습니다. 이 방식은 탭조이가 관리하는 재화에 비해 더 높은 수준의 제어권을 제공하지만, 재화 잔액 저장 및 업데이트를 포함한 모든 백엔드 프로세스를 직접 처리해야 합니다.

탭조이에서 관리하는 재화와 달리, 자체 관리 재화는 `getCurrencyBalance`, `awardCurrency`, `spendCurrency`와 같은 기능을 지원하지 않습니다. 또한, 탭조이는 자체 관리 통화에 대해서는 클라이언트 측 또는 앱 측 알림 기능을 제공하지 않습니다. 탭조이가 귀하의 콜백 서버에 접속할 때 앱과 사용자에게 이를 알리는 것은 여러분의 책임입니다.

자율 관리형 재화는 다음과 같은 고유한 요구 사항이 있습니다.

* 지연된 보상: 탭조이는 사용자에게 최대한 신속하게 보상을 제공하기 위해 노력하고 있으나, 즉시 지급을 보장할 수는 없습니다. 잔액 정보가 정확하게 업데이트되도록 하려면, 앱 실행 시, 재개 이벤트 발생 시, 레벨 전환 시, 동영상 광고 시청 완료 시, 스토어 이용 전과 같은 정기적인 시점에 변경 사항이 있는지 확인하십시오. 사용자들에게 오퍼를 완료한 후 보상이 표시되기까지 다소 시간이 걸릴 수 있음을 안내합니다.
* 플랫폼별 재화: 각 플랫폼(iOS, Android 등)은 자체적으로 관리하는 경우라도 각각 고유한 재화 설정이 필요합니다.

## 콜백 URL##callback-url

사용자가 오퍼를 통해 재화를 획득하면, 탭조이는 지정한 콜백 URL로 HTTP GET 요청을 전송합니다. 요청에는 다음 파라미터가 포함됩니다.

* `snuid`: 사용자 ID입니다.
* `currency`: 사용자 계정에 입금할 재화 금액.
* `mac_address`: 사용자의 Wi-Fi MAC 주소(가능한 경우)

다음 콜백 예시를 참고하십시오.

```java
  <callback_url>?snuid=<user_id>&currency=<currency>&mac_address=<mac_address>

  // Example
  http://www.sampledoman.com/payments/offers/tapjoy?&snuid=42&currency=50&mac_address=00-16-41-34-2C-A6
```

## 서버 응답 요구 사항##server-response-requirements

서버는 다음 HTTP 상태 코드 중 하나로 응답해야 합니다.

| 상태 코드           | 원인                                                                                         |
| --------------- | ------------------------------------------------------------------------------------------ |
| `200 OK`        | 사용자가 재화를 받았습니다.                                                                            |
| `403 Forbidden` | * verifier 파라미터가 예상 값과 일치하지 않습니다.
* snuid 파라미터를 인식할 수 없습니다.
* 추가 재시도 시도를 막는 기타 오류가 발생했습니다. |

탭조이는 다음과 같은 상황에서 요청을 다시 시도합니다.

* 탭조이가 `200` 또는 `403` 응답을 받지 못하면, 최대 4일 동안 5분 간격으로 요청을 재시도합니다.
* 서버의 응답 시간이 5초를 초과할 경우, 탭조이는 해당 요청을 실패로 간주하고 재시도합니다.
* 응답은 UTF-8로 인코딩되어야 합니다. 그렇지 않으면 서버에서 `200`를 반환하더라도 탭조이는 재시도합니다.

> **Note:**
>
> 서버에서 200이 아닌 응답을 반환하는 경우 사용자에게 재화를 지급하지 마십시오. 탭조이는 계속 재시도할 것이며, 이로 인해 보상이 중복될 수 있습니다.

## 부정 행위 감지 또는 방지 파라미터##fraud-detection-or-prevention-parameters

보안 강화를 위해 가상 재화 비밀 키를 사용하십시오. 탭조이 대시보드에서 **Monetize** > **Virtual Currency** > **Create/Edit**에서 비밀 키를 설정합니다. 이 키는 애플리케이션 SDK 키와 다르며, 콜백에 서명하는 데 사용됩니다.

가상 재화 비밀 키가 있는 재화에는 다음과 같은 추가 보안 파라미터가 있습니다.

* ID: 보상 요청에 대한 고유 식별자(`currency_id`와는 다름)
* 검증자: ID, snuid, 통화, 그리고 비밀 키를 MD5 해시로 변환한 값입니다.

### 모니터링해야 할 부정 행위 시나리오##fraud-scenarios-to-monitor

통화를 관리할 때 다음과 같은 부정 행위 시나리오를 고려합니다.

* ID가 재사용되거나 검증자가 잘못된 경우, 콜백 URL이 탭조이에서 온 것이 아닐 수 있으므로 부정 행위로 간주할 수 있습니다.
* 사용자 ID가 정수만 포함되어 있다면, snuid를 검증하여 조작을 방지합니다. 예를 들어, 탭조이는 `001234`와 `1234`를 서로 다른 두 개의 사용자 ID로 간주합니다. 하지만 백엔드 서버 로직은 그렇지 않을 수도 있습니다.

## 검증자 계산##verifier-calculation

검증자는 다음 형식의 MD5 해시입니다.

```java
Digest::MD5.hexdigest("#{id}:#{snuid}:#{currency}:#{secret_key}")
```

서버는 검증자를 재계산해야 하며, 요청 중 일치하지 않는 항목이 있을 경우 `403 Forbidden` 응답을 반환해야 합니다. 애플리케이션 간 보상 관련 문제를 방지하려면 각 애플리케이션마다 고유한 비밀 키를 사용하십시오.

각 애플리케이션에는 별도의 비밀 키가 있어야 합니다. 모든 애플리케이션에 동일한 비밀 키를 사용하지 마십시오. 이렇게 하면 여러 애플리케이션에 걸쳐 있는 사용자에게 동시에 보상이 지급될 수 있습니다.

## 콜백 URL 개선##improved-callback-url

탭조이 고객 지원 팀을 통해 더 안전한 콜백 형식을 이용할 수 있습니다. 이 버전은 JSON 페이로드가 포함된 POST 요청과 SHA256 기반 HMAC(Hash-Based Message Authentication Code) 인증 방식을 사용합니다. 이 기능을 활성화하려면 세일즈 담당자나 지원 팀에 문의해 주십시오.

사용자가 재화를 획득하면, 탭조이는 지정한 URL로 POST 요청을 보냅니다. 탭조이는 요청 본문과 공유한 비밀 키(탭조이 대시보드에서 확인 가능)를 기반으로 검증자를 생성하여 요청 헤더에 포함시킵니다.

탭조이는 다음 예시와 유사한 구조를 사용하여 요청에 서명합니다.

```javascript
{
   "cp": "some_string",
   "currency": {
      "currency_sale": "1.0",
      "id": "reward_id",
      "reward": 100
   },
   "id": "some_id",
   "offer": {
      "advertiser_app_name": "a_cool_app",
      "currency": {
         "max_reward_value": 1360
      },
      "expires_at": 1768175428,
      "icon_url": "some_URL/icon.jpg",
      "name": "eye_catching_headline",
      작업
         "is_iap": false,
         "name": "event_name"
      },
      type
   },
   "placement":{
      "content_type": "offerwall",
      "name": "placement_name"
   },
   "rev": 100,
   "timestamp": 1762993750,
   "user": {
      "id": "user_id"
   }
}
```

| 파라미터&#xA;Type&#xA;Description |

| `id`&#xA;문자열&#xA;이 요청에 대한 고유한 보상 ID |

| `rev`&#xA;double&#xA;USD 달러로 획득한 매출 |

| `cp`&#xA;문자열&#xA;setCustomParameter 메서드를 통해 SDK로 전달된 커스텀 파라미터 |

| `currency.id`&#xA;문자열&#xA;이 통화의 고유 식별자 |

| `currency.reward`&#xA;정수&#xA;사용자에게 지급된 통화 금액 |

| `currency.currency_sale`&#xA;float&#xA;재화 세일 멀티플라이어(해당되는 경우) |

| `currency.max_reward_value`&#xA;정수&#xA;사용자가 이 오퍼의 모든 작업을 완료하면 얻을 수 있는 최대 재화입니다. |

| `offer.name`&#xA;문자열&#xA;광고주 오퍼 이름 |

| `offer.type`&#xA;문자열&#xA;현재 지원되지 않음 |

| `offer.icon_url`&#xA;문자열&#xA;오퍼의 아이콘 URL |

| `offer.advertiser_app_name`&#xA;문자열&#xA;광고주 앱 이름 |

| `offer.task.name`&#xA;문자열&#xA;사용자가 보상을 받는 완료된 작업의 이름 또는 설명입니다. |

| `offer.task.is_iap`&#xA;부울&#xA;보상이 제공되는 이벤트가 IAP 이벤트인 경우 true |

| `offer.expires_at`&#xA;timestamp&#xA;사용자가 더 이상 이 오퍼에 대한 보상을 받을 수 없는 타임스탬프(초)입니다. |

| `placement.name`&#xA;문자열&#xA;이 전환과 관련된 플레이스먼트 |

| `placement.content_type`&#xA;문자열&#xA;사용되는 플레이스먼트 유형 |

| `user.id`&#xA;문자열&#xA;setUserID 메서드를 통해 SDK를 통해 전달되는 사용자 ID |

| `timestamp`&#xA;timestamp&#xA;이 트랜잭션의 타임스탬프 |

탭조이는 다음 형식을 사용하여 요청을 전송합니다.

```java
HMAC_SHA-256(<Request-Body>,<Secret-Key>)
```

다음 예시는 서명이 포함된 탭조이 헤더를 보여 줍니다.

```java
X-Tapjoy-Signature => 7205ccfdfa1fe28cd05a1b56a9508d898cc938aa555a6c18848097fe4ee0975b
```

## 사용자 ID 설정##setting-the-user-id

사용자가 직접 관리하는 재화의 경우, 사용자가 보상을 받을 수 있도록 탭조이 상호 작용 전에 고유한 `user_id` 기호를 설정하는 것이 중요합니다. 이 값은 콜백 URL의 snuid가 됩니다. 콘텐츠 요청을 보내기 전, 초기 연결 단계에서 연결 플래그와 함께 `user_id`를 설정합니다. 설정이 잘못되면 사용자는 보상을 받을 수 없으며, 수익을 얻을 수 없습니다.

재화 기호로 `user_id`를 설정할 때는 다음 요구 사항을 참고하십시오.

* 고유한 숫자 ID(최대 190자)를 사용합니다.
* GDPR 준수를 위해 식별 가능한 정보(예: 사용자 이름, 실명, 이메일 주소)를 포함하지 않습니다.
* 보안 및 사기 탐지를 위해 각 사용자의 전체 이용 기간 동안 ID를 일관되게 유지합니다.

이 설정이 지정되지 않은 경우, 탭조이는 기본적으로 디바이스 ID(일반적으로 광고 ID)를 사용하며, 이는 SDK 버전, 디바이스 또는 운영 체제에 따라 달라질 수 있습니다.

다음 코드 예제는 연결 플래그를 사용하는 방법과, 필요한 경우 (연결 후) setUserID API를 직접 호출하는 방법을 보여 줍니다. API를 직접 호출할 때는 콜백을 사용하여 ID가 성공적으로 설정되었는지 확인합니다. 가능한 경우 연결 플래그를 사용하는 것이 권장되는 베스트 프랙티스입니다.

1. **iOS**

   ```objc title="Objective-C"
   // Recommended approach using connect flag
   NSDictionary *connectFlags = @{TJC_OPTION_USER_ID : @"<USER_ID_HERE>"};
   [Tapjoy connect:@"SDK_KEY_GOES_HERE" options:connectFlags];

   // Setting the user id directly
   [Tapjoy setUserIDWithCompletion:@"<USER_ID_HERE>" completion:^(BOOL success, NSError *error) {

   }];
   ```

2. **Android**

   ```java title="Java" 
   // Recommended approach using connect flag 
   Hashtable<string, object>connectFlags = new Hashtable&lt;string, object>(); 
   connectFlags.put(TapjoyConnectFlag.USER_ID, "<USER_ID_HERE>"); // Important for self-managed currency

   Tapjoy.connect(getApplicationContext(), "SDK_KEY_GOES_HERE", connectFlags, new TJConnectListener() {...});

   // Setting the user id directly

   Tapjoy.setUserID("<USER_ID_HERE>", new TJSetUserIDListener() {
     @Override
     public void onSetUserIDSuccess() {
       
     }

     @Override
     public void onSetUserIDFailure(String error) {

     }
   });
   ```

3. **Unity**

   ```c# title="C-sharp"
   // Recommended approach using connect flag
   Dictionary&lt;string,string> connectFlags = new Dictionary&lt;string,string>();
   connectFlags.Add("TJC_OPTION_USER_ID", "<USER_ID_HERE>");

   #if UNITY_ANDROID
     Tapjoy.Connect("your_android_sdk_key", connectFlags);
   #elif UNITY_IOS
     Tapjoy.Connect("your_ios_sdk_key", connectFlags);
   #endif

   // Callbacks for SetUserID
   TJPlacement.OnSetUserIDSuccess += HandleOnSetUserIDSuccess;
   TJPlacement.OnSetUserIDFailure += HandleOnSetUserIDFailure;

   // Setting the user id directly
   Tapjoy.SetUserID("<USER_ID_HERE>")
   ```

4. **React Native**

   ```js title="Javascript"
   // Recommended approach using connect flag
   try {
     let flags: object = { TJC_OPTION_USER_ID: '<userId>' };
     await Tapjoy.connect('<sdk_key>', flags);
   } catch (error) {
     console.log(error);
   }

   // Setting the user id directly
   try {
     await Tapjoy.setUserId('<userId>');
   } catch (error) {
     console.log(error);
   }
   ```

각 플랫폼의 Quickstart 페이지에서 더 많은 예제를 확인할 수 있습니다.

## 사용자 잔액 설정##setting-the-user-balance

콘텐츠를 로드하기 전에, 플레이스먼트를 요청할 때마다 탭조이에 사용자의 현재 잔액을 업데이트합니다. 이렇게 하면 자체 관리형 재화를 정확하게 추적할 수 있습니다.

1. **iOS**

   ```objc title="Objective-C"
   TJPlacement *placement = [TJPlacement placementWithName:@"placementName" delegate:nil];
   [placement setBalance:100 forCurrencyId:@"1234" withCompletion:^(NSError * _Nullable error) {
       if (error != nil) {
           //Failure
           NSString *message = error.localizedDescription;
       } else {
           //Success
       }
   }];
   ```

2. **Android**

   ```java title="Java"
   TJPlacement placement = Tapjoy.getPlacement("placement", this);
   placement.setCurrencyBalance("1234", 100, new TJSetCurrencyBalanceListener() {
       @Override
       public void onSetCurrencyBalanceSuccess() {
           
       }

       @Override
       public void onSetCurrencyBalanceFailure(int code, String error) {

       }
   }); 
   ```

3. **Unity**

   ```c# title="C-sharp"
   TJPlacement placement = TJPlacement.CreatePlacement("placementName");
   placement.SetCurrencyBalance("[CURRENCY_ID]", 100);

   // Optional callbacks

   void OnEnable() 
   {	
       TJPlacement.OnSetCurrencyBalanceSuccess += HandleSetCurrencyBalanceSuccess;	
       TJPlacement.OnSetCurrencyBalanceFailure += HandleSetCurrencyBalanceFailure;	
   }

   void OnDisable() 
   {	
       TJPlacement.OnSetCurrencyBalanceSuccess -= HandleSetCurrencyBalanceSuccess;	
       TJPlacement.OnSetCurrencyBalanceFailure -= HandleSetCurrencyBalanceFailure;
   }

   public void HandleSetCurrencyBalanceSuccess(TJPlacement placement) 
   {

   }

   public void HandleSetCurrencyBalanceFailure(TJPlacement placement, int code, string error)
   {

   }
   ```

4. **React Native**

   ```js title="Javascript"
   let placement = new TJPlacement('placementName');
   try {
     await placement?.setCurrencyBalance('1234', 100);
   } catch (e: any) {
     let code = e.code;
     let message = e.message;
   }
   ```

## 필수 금액##required-amount

잔액을 설정하면, 플레이스먼트 항목에 필요한 금액 값도 설정할 수 있습니다.

1. **iOS**

   ```objc title="Objective-C"
   TJPlacement* placement = [TJPlacement placementWithName:@"placementName" delegate:nil];
   placement setRequiredAmount:100 forCurrencyId:@"1234" withCompletion:^(NSError * _Nullable error) {
       if (error != nil) {
           //Failure
           NSString *message = error.localizedDescription;
       } else {
           //Success
       }
   } 
   ```

2. **Android**

   ```java title="Java"
   TJPlacement placement = Tapjoy.getPlacement("placement", this);
   placement.setCurrencyAmountRequired("1234", 100, new TJSetCurrencyAmountRequiredListener() {
       @Override
       public void onSetCurrencyAmountRequiredSuccess() {
           
       }

       @Override
       public void onSetCurrencyAmountRequiredFailure(int code, String error) {

       }
   }); 
   ```

3. **Unity**

   ```c# title="C-sharp"
   TJPlacement placement = TJPlacement.CreatePlacement("placementName");
   placement.SetRequiredAmount("[CURRENCY_ID]", 200); 

   // Optional callbacks

   void OnEnable() 
   {	
       TJPlacement.OnSetCurrencyAmountRequiredSuccess += HandleSetRequiredAmountSuccess;	
       TJPlacement.OnSetCurrencyAmountRequiredFailure += HandleSetRequiredAmountFailure;}

   void OnDisable() 
   {	
       TJPlacement.OnSetCurrencyAmountRequiredSuccess -= HandleSetRequiredAmountSuccess;	
       TJPlacement.OnSetCurrencyAmountRequiredFailure -= HandleSetRequiredAmountFailure;
   }

   public void HandleSetCurrencyBalanceSuccess(TJPlacement placement)
   {

   }

   public void HandleSetCurrencyBalanceFailure(TJPlacement placement, int code, string error)
   {

   }

   public void HandleSetRequiredAmountSuccess(TJPlacement placement)
   {

   }
   public void HandleSetRequiredAmountFailure(TJPlacement placement, int code, string error)
   {

   } 
   ```

4. **React Native**

   ```javascript title="Javascript"
   let placement = new TJPlacement('placementName');
   try {
   await offerwallPlacement?.setRequiredAmount(100, '100');
   } catch (e: any) {
     let code = e.code;
     let message = e.message;
   } 
   ```

## 보상 콜백 IP allowlist##reward-callback-ip-allowlisting

콜백 서버에서 액세스를 제한하는 경우, 다음 탭조이 IP 주소를 allowlist에 추가합니다.

> **Note:**
>
> 이 목록은 2024년 5월 12일 일요일 기준 최신 정보입니다.

* `18.215.207.89`
* `18.235.142.165`
* `23.20.255.113`
* `23.23.134.165`
* `3.210.188.32`
* `3.215.42.140`
* `3.217.209.177`
* `3.218.95.35`
* `3.219.236.53`
* `3.231.137.161`

## 문제 해결##troubleshooting

콜백 URL에서 예상치 못한 snuid를 받으면, connect 호출 후 앱 실행 시마다 `setUserID`를 호출하고 사용자가 오퍼에 액세스하기 전에 실행해야 합니다.

디바이스에 사용자 ID가 연결되어 있지 않은 경우, 탭조이는 콜백 URL의 snuid로 디바이스 ID를 전송합니다. 예를 들어, 사용자가 앱을 실행한 뒤 앱이 userID를 전송하기 전에 탭조이 웹사이트로 이동할 수도 있습니다. 이를 방지하려면, 서버가 connect 호출 후 실행 시마다 `setUserID`를 호출하도록 해야 합니다.

사용자 ID를 설정하지 않은 경우 시스템은 사용 가능한 최적의 디바이스 ID를 사용하려 시도합니다. 대부분의 경우 이는 디바이스의 광고 ID입니다. 단, SDK 버전, 디바이스 모델/버전, 디바이스 운영 체제(OS) 버전 및 Google Play 서비스에 따라 정확한 ID는 달라질 수 있습니다. 다른 가능한 값으로는 Android ID, udid, mac\_address 등이 있습니다.
