> For the complete documentation index, see [llms.txt](https://onestore-dev.gitbook.io/dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onestore-dev.gitbook.io/dev/tools/web-sdk/iap.md).

# 05. 인앱 결제 (IAP)

### **사전 확인**

#### 인앱 상품 등록

ONEconsole의 "앱 수익화 > 인앱 상품" 메뉴에서 **관리형상품**을 등록해야 합니다. Web 상품에서 인앱결제가 지원하는 상품은 관리형상품이며, 구독상품은 지원하지 않습니다. 자세한 내용은 [02. 사전준비](/dev/tools/web-sdk/pre.md)를 참고하시기 바랍니다.

#### 기능 지원 여부 확인: isSupported

`createSDK()`로 얻은 SDK 객체의 `iap` 모듈로 구매/결제와 상품 정보 조회를 요청합니다. 기능 지원 여부는 `sdk.iap.isSupported(feature)`로 확인할 수 있으며, **구매 UI나 상점 가격을 노출하기 전에 확인하십시오.**

<table><thead><tr><th width="320">호출</th><th width="100">반환</th><th>의미</th></tr></thead><tbody><tr><td><code>sdk.iap.isSupported()</code></td><td>boolean</td><td>결제 지원 여부 (<code>"purchase"</code>와 동일)</td></tr><tr><td><code>sdk.iap.isSupported("purchase")</code></td><td>boolean</td><td>결제 지원 여부</td></tr><tr><td><code>sdk.iap.isSupported("getProductDetails")</code></td><td>boolean</td><td>상품 정보 조회 지원 여부 (아래 '상품 정보 조회' 절 참고)</td></tr></tbody></table>

```javascript
// initializeAsync() 이후에 확인하고, 지원하는 경우에만 구매 UI를 노출합니다
if (sdk.iap.isSupported("purchase")) {
  showPurchaseButton();
}
```

* 판정 근거는 초기화 응답으로 확정되므로 `initializeAsync()` **이후**에 호출하십시오. 그 전에는 낙관적으로 true가 반환될 수 있습니다.
* 결제와 상품 정보 조회는 앱 버전에 따라 지원 여부가 **따로** 갈릴 수 있습니다. 인자 없는 `isSupported()`는 결제만 뜻하므로, 상품 정보 조회 전에는 반드시 `isSupported("getProductDetails")`로 확인하십시오.

### **구매/결제 요청: purchase**

구매/결제는 `purchase()` 호출 한 번으로 요청합니다. 성공 시 결과 객체가 반환되며, **사용자 취소 · 결제 실패 · 요청 거절은 모두 오류로 전달됩니다** ([06. 오류 사유 (reason)](/dev/tools/web-sdk/errors.md)의 구매/결제 · 상품 정보 조회 오류 참고). 결제가 종료될 때까지 응답이 지연될 수 있습니다.

#### 구매/결제 예시

```javascript
var sdk = createSDK();
// ... 중략 ...

var param = {
  productId: "gem10000+10",
  productName: "보석1000+보너스10",
  developerPayload: "{{Optional: 아이템 지급 처리 시 사용할 정보}}"
};

sdk.iap.purchase(param).then(
  function (result) {
    console.info("구매/결제 성공");
    console.info("결제ID (영수증 정보): ", result.orderId);
    console.info("결제 수행 시각 (epoch millis): ", result.purchaseTimeMillis);
    console.info("구매 ID: ", result.purchaseId);
    console.info("구매 토큰: ", result.purchaseToken);
    console.info("구매/결제 요청한 인앱 상품의 ID: ", result.productId);
    console.info("developerPayload: ", result.developerPayload);

    // TODO: 아이템 지급 확인
  },
  function (error) {
    console.error("구매/결제 오류 확인 >", error.reason);
  }
);
```

#### 구매/결제 요청 파라미터

`sdk.iap.purchase(param)`의 `param`은 JSON(객체) 형식입니다.

<table><thead><tr><th width="170">파라미터</th><th width="90">타입</th><th width="100">필수 여부</th><th>설명</th></tr></thead><tbody><tr><td>productId</td><td>String</td><td>필수</td><td>결제를 수행할 인앱 상품의 ID (개발자 센터를 통해 개발사가 등록한 custom PID)</td></tr><tr><td>productName</td><td>String</td><td>선택</td><td>결제를 수행할 인앱 상품의 상품명 (미전달 시 개발자 센터에 등록한 상품명 노출)</td></tr><tr><td>developerPayload</td><td>String</td><td>선택</td><td>결제 결과를 게임에서 수신 또는 <a href="https://onestore-dev.gitbook.io/dev/tools/billing/v21/pns">PNS</a>로 통지 받을 때 식별하기 위한 개발자 정의 문구 (최대 200바이트)</td></tr></tbody></table>

#### 구매/결제 응답 결과

<table><thead><tr><th width="190">키</th><th width="90">타입</th><th>설명</th></tr></thead><tbody><tr><td>orderId</td><td>String</td><td>결제ID (영수증 정보)</td></tr><tr><td>purchaseTimeMillis</td><td>Number</td><td>결제 수행 시각 (epoch millis)</td></tr><tr><td>purchaseId</td><td>String</td><td>구매 ID</td></tr><tr><td>purchaseToken</td><td>String</td><td>구매 토큰</td></tr><tr><td>productId</td><td>String</td><td>구매/결제 요청한 인앱 상품의 ID</td></tr><tr><td>developerPayload</td><td>String</td><td>구매건을 식별하기 위해 개발사에서 관리하는 식별자 (요청 시 전달한 developerPayload)</td></tr></tbody></table>

{% hint style="danger" %}
**지급 확정 원칙**

* `purchase()`의 성공 응답은 **결제 완료 신호일 뿐, 아이템 지급 근거가 아닙니다.** 실제 지급은 게임사 서버가 `purchaseToken`으로 결제를 검증한 뒤 확정하십시오.
* 결제 완료는 [PNS](https://onestore-dev.gitbook.io/dev/tools/billing/v21/pns)로 게임사 서버에 통지됩니다. 게임 클라이언트가 응답을 받지 못한 경우(네트워크 단절 · 앱 강제 종료 등)에도 PNS는 도달하므로, 지급 처리는 서버 기준으로 구성해야 유실이 없습니다.
* 소비(consume) 처리도 게임사 서버 몫입니다. 앱은 결제 후 자동 소비를 하지 않으므로, **소비하지 않은 상품을 다시 구매하면 `already_owned`로 실패**합니다. 관련 API는 [원스토어 인앱결제 서버 API](https://onestore-dev.gitbook.io/dev/tools/billing/v21/serverapi)를 참고하십시오.
* `developerPayload`는 주문과 유저를 잇는 식별자입니다. PNS 수신 시 어느 구매건인지 대조하려면 요청 시 전달하고 서버에 매핑해 두십시오.
  {% endhint %}

### **상품 정보 조회: getProductDetailsAsync**

게임 내 상점에 가격을 표시할 때 사용합니다. 개발자센터에 등록한 인앱 상품 ID 목록을 전달하면 상품명과 가격을 돌려받습니다. 결제 화면을 띄우지 않으므로 광고 · 결제 표시 중에도 호출할 수 있습니다.

호출 전 `sdk.iap.isSupported("getProductDetails")`로 지원 여부를 확인하십시오. 브라우저 환경이거나 앱 버전이 낮으면 `false`를 반환하며, 이때는 호출하지 말고 가격 표시 없이 진행하는 등 게임에서 대체 처리하십시오.

#### 상품 정보 조회 예시

```javascript
if (sdk.iap.isSupported("getProductDetails")) {
  sdk.iap.getProductDetailsAsync(["ruby_100", "ruby_500"])
    .then(function (products) {
      // products: [{ productId, type, title, price, priceAmountMicros, priceCurrencyCode }, ...]
      products.forEach(function (p) {
        showPrice(p.productId, p.price, p.priceCurrencyCode);
      });
    })
    .catch(function (e) {
      console.warn("상품 정보 조회 실패", e.reason, e.message);   // 오류 사유 참고
    });
}
```

#### 요청 파라미터

<table><thead><tr><th width="130">파라미터</th><th width="90">타입</th><th width="80">필수</th><th>설명</th></tr></thead><tbody><tr><td>productIds</td><td>string[]</td><td>필수</td><td>조회할 인앱 상품 ID 목록. <code>purchase()</code>의 <code>productId</code>와 같은 값입니다. <strong>1회 최대 20개</strong>이며, 초과 · 빈 배열 · 빈 문자열 포함 · 배열이 아닌 값은 앱 호출 없이 <code>invalid_request</code>로 실패합니다. 21개 이상은 20개 단위로 나눠 호출하십시오</td></tr></tbody></table>

#### 응답: 상품 객체 배열

상품 1개 = 객체 1개. 판매 중인 상품만 담기며, 없으면 빈 배열입니다.

<table><thead><tr><th width="190">필드</th><th width="90">타입</th><th>설명</th></tr></thead><tbody><tr><td>productId</td><td>string</td><td>인앱 상품 ID. 요청에 담아 보낸 값</td></tr><tr><td>type</td><td>string</td><td>상품 타입. <code>"inapp"</code> 고정</td></tr><tr><td>title</td><td>string</td><td>상품명 (개발자센터 등록명)</td></tr><tr><td>price</td><td>string</td><td>가격 문자열 (예: <code>"1000"</code>). 통화 기호 · 자릿수 구분은 포함되지 않을 수 있으니 <code>priceCurrencyCode</code>와 함께 표기하십시오</td></tr><tr><td>priceAmountMicros</td><td>number</td><td>가격 × 1,000,000. 금액 계산 · 비교 · 정렬에 사용</td></tr><tr><td>priceCurrencyCode</td><td>string</td><td>통화 코드 (<code>"KRW"</code> 등)</td></tr></tbody></table>

{% hint style="warning" %}
**조회 결과 처리 원칙**

* **빈 배열은 오류가 아닙니다.** 요청한 상품이 하나도 조회되지 않아도 정상 응답(빈 배열)으로 옵니다. 빈 배열과 오류를 같은 분기로 처리하지 마십시오.
* **일부만 조회될 수 있습니다.** 판매 중지 · 미등록 상품은 결과에서 빠지며, 이는 오류가 아닙니다. **결과 순서는 요청 순서와 일치하지 않을 수 있습니다.** `productId`로 대조하십시오.
* 화면 표시는 `price`, **금액 계산 · 정렬은 `priceAmountMicros`**&#xB97C; 사용하십시오. `price`는 문자열이므로 산술에 쓰면 안 됩니다.
* 실패는 Promise reject로 전달됩니다 ([06. 오류 사유 (reason)](/dev/tools/web-sdk/errors.md) 참고). 10초 안에 응답이 없으면 `reason: "timeout"`입니다.
  {% endhint %}
