> For the complete documentation index, see [llms.txt](https://workspace-help.nhn-commerce.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://workspace-help.nhn-commerce.com/contents/recommended/ios-aos.md).

# 결제 모듈 앱 연동 가이드(iOS/AOS)

본 가이드는 자체 iOS/AOS 앱에 샵바이 결제편의모듈(NCPPay)을 연동하기 위한 가이드 입니다.

### 1. 결제 흐름도

<figure><img src="https://67612295-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLt0GowPleXLWRjzGIAiV%2Fuploads%2Feahs2YmgbeX96Nj3SmDX%2F%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202026-09-23%20141910.png?alt=media&amp;token=4babda06-9087-4049-b695-5f05284a2d3d" alt=""><figcaption></figcaption></figure>

결제 흐름은 다음과 같습니다.

1. 고객이 주문서를 작성하고 결제를 요청하면 Webview에서 로컬 payment.html을 로드합니다.
2. 클라이언트가 요청할 PG사 및 결제 정보를 NCPPay 서버에 전달하고, 서버는 해당 PG사에 결제 정보를 생성합니다.
3. 해당 PG사의 결제창이 로드되고 결제가 진행됩니다.
4. PG사가 결제 승인 요청 및 결제 결과를 NCPPay 서버로 전달합니다.
5. NCPPay 서버가 클라이언트의 confirmUrl을 호출하고, 클라이언트는 결제 결과 페이지를 노출합니다.

### 2. 설명

* 샵바이 결제편의 모듈은 iOS/AOS 애플리케이션 내에 **WebView**를 이용하여 연동합니다.
* WebView에서 샵바이 결제편의 모듈을 띄우는 방식은 기본 웹 연동(PC·모바일웹) 방법과 유사합니다. PC 연동 가이드를 참고하세요.
  * \[개별형 스킨] <https://workspace-help.nhn-commerce.com/aurora-guide/api-1/order-sheet-form>
  * \[통합형 스킨] <https://nhnent.dooray.com/share/pages/WoOk8q6KT1K3MZcLSuyZsw/3520341896217443973>

연동 방식은 두 가지로 나뉩니다.

| 방식             | 설명                                                                                                        |
| -------------- | --------------------------------------------------------------------------------------------------------- |
| **Web(하이브리드)** | 자체 모바일웹(PC 연동 가이드대로 구성된 주문서 페이지)을 WebView로 그대로 띄우는 방식. 앱 쪽에서는 스킴 등록, 딥링크, 쿠키 설정 등 최소한의 네이티브 설정만 추가하면 됩니다. |
| **Native**     | 앱이 직접 `NCPPay.setConfiguration` / `reservation` 을 호출하여 결제를 실행하는 방식. 스크립트 호출용 html을 앱에서 직접 준비해야 합니다.       |

### 3. 은행 및 카드사 앱 패키지/스킴 정보

앱 연동 시(Web·Native 공통), 카드사 결제 앱 호출을 위해 아래 패키지명(AOS)·URL 스킴(iOS) 정보가 필요합니다.

#### 3-1. 결제 App별 패키지명 (AOS)

| App              | 패키지명                                        | APP       | 패키지명                         |
| ---------------- | ------------------------------------------- | --------- | ---------------------------- |
| 페이코              | com.nhnent.payapp                           | ISP       | kvp.jjy.MispAndroid320       |
| SSGPAY           | com.ssg.serviceapp.android.egiftcertificate | KBPay     | com.kbcard.cxh.appcard       |
| LPAY             | com.lottemembers.android                    | liivbank  | com.kbstar.liivbank          |
| KPAY             | com.inicis.kpay                             | newliiv   | com.kbstar.reboot            |
| 티머니페이            | com.tmoney.tmpay                            | KB 스타뱅킹   | com.kbstar.kbbank            |
| 토스페이             | viva.republica.toss                         | 삼성페이      | com.samsung.android.spay     |
| 삼성페이             | com.samsung.android.spay                    | 삼성페이미니    | com.samsung.android.spaylite |
| 카카오페이            | com.kakao.talk                              | 엘지페이      | com.lge.lgpay                |
| 차이페이             | finance.chai.app                            | 하나        | com.hanaskcard.paycla        |
| 네이버              | com.nhn.android.search                      | 롯데        | com.lcacApp                  |
| 삼성               | kr.co.samsungcard.mpocket                   | 신한        | com.shcard.smartpay          |
| 신한(ARS/일반/smart) | com.shinhancard.smartshinhan                | 현대        | com.hyundaicard.appcard      |
| 농협               | nh.smart.nhallonepay                        | 씨티        | kr.co.citibank.citimobile    |
| 우리 WON 카드        | com.wooricard.smartapp                      | 우리 WON 뱅킹 | com.wooribank.smart.npib     |

**신용카드 백신 App 패키지명**

| 백신명     | 패키지명                       |
| ------- | -------------------------- |
| 터치엔 엠백신 | com.TouchEn.mVaccine.webs  |
| V3      | com.ahnlab.v3mobileplus    |
| V-Guard | kr.co.shiftworks.vguardweb |

**신용카드 공인인증 App 패키지명**

| 카드사 | 패키지명                         |
| --- | ---------------------------- |
| 하나  | com.hanaskcard.rocomo.potal  |
| 현대  | com.lumensoft.touchenappfree |

**계좌이체 App 패키지명**

| 계좌이체 | 패키지명                     |
| ---- | ------------------------ |
| 뱅크페이 | com.kftc.bankpay.android |
| 케이뱅크 | com.kbankwith.smartbank  |

**해외결제 App 패키지명**

| 해외결제 | 패키지명                        |
| ---- | --------------------------- |
| 알리페이 | com.eg.android.AlipayGphone |
| 위챗페이 | com.tencent.mm              |

#### 3-2. 은행 및 카드사 앱 Scheme 정보 등록 (iOS)

iOS9부터 보안 강화 목적으로 앱을 호출할 때 앱스키마를 등록해주어야 동작합니다. `info.plist` 파일의 `LSApplicationQueriesSchemes` 키에 각 PG사에서 제공하는 앱 스키마 리스트를 등록해야 합니다.

스키마 리스트(INICIS 제공):

```xml
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>shinhan-sr-ansimclick</string>
    <string>smshinhanansimclick</string>
    <string>hdcardappcardansimclick</string>
    <string>smhyundaiansimclick</string>
    <string>mpocket.online.ansimclick</string>
    <string>scardcertiapp</string>
    <string>cloudpay</string>
    <string>hanawalletmembers</string>
    <string>hanaskcardmobileportal</string>
    <string>nhallonepayansimclick</string>
    <string>nonghyupcardansimclick</string>
    <string>kb-acp</string>
    <string>kbbank</string>
    <string>newliiv</string>
    <string>Liivbank</string>
    <string>NewSmartPib</string>
    <string>com.wooricard.wcard</string>
    <string>wooripay</string>
    <string>citimobileapp</string>
    <string>lotteappcard</string>
    <string>lottesmartpay</string>
    <string>ispmobile</string>
    <string>kpay</string>
    <string>payco</string>
    <string>paycoapplogin</string>
    <string>kakaotalk</string>
    <string>lpayapp</string>
    <string>lmslpay</string>
    <string>shinsegaeeasypayment</string>
    <string>supertoss</string>
    <string>tmoneypay</string>
    <string>chai</string>
    <string>tswansimclick</string>
    <string>uppay</string>
    <string>kftc-bankpay</string>
    <string>bankwallet</string>
    <string>ukbanksmartbanknonloginpay</string>
    <string>kdb-bankpay</string>
    <string>ibk-bankpay</string>
    <string>kb-bankpay</string>
    <string>keb-bankpay</string>
    <string>sh-bankpay</string>
    <string>nhb-bankpay</string>
    <string>nh-bankpay</string>
    <string>wr-bankpay</string>
    <string>sc-bankpay</string>
    <string>s-bankpay</string>
    <string>ct-bankpay</string>
    <string>dg-bankpay</string>
    <string>bnk-bankpay</string>
    <string>kj-bankpay</string>
    <string>jj-bankpay</string>
    <string>jb-bankpay</string>
    <string>kn-bankpay</string>
    <string>kp-bankpay</string>
    <string>cu-bankpay</string>
    <string>mg-bankpay</string>
    <string>kbn-bankpay</string>
    <string>kkb-bankpay</string>
</array>
```

### 4. Web(하이브리드) 방식 연동

#### 4-1. iOS (Swift)

* iOS12 이상
* WebView를 이용한 하이브리드앱이며, 각 PG사에서 제공하는 모바일앱 결제 연동 가이드를 참고해 주세요.
* 앱 스킴 등록은 3-2. 은행 및 카드사 앱 Scheme 정보 등록을 참고하세요.

**결제편의 모듈의 Reservation 값 설정**

Native 방식과 동일하게 `inAppYn`, `extraData`를 설정해야 카드사앱 결제 후 자동으로 원래 앱으로 돌아옵니다. (내용은 5-2 참고)

**info.plist 파일에 딥링크(URL Scheme) 설정 / 쿠키 설정**

Native 방식과 동일합니다. (5-2 참고)

**웹페이지 탐색 허용 여부 설정**

iOS에서 일부 은행 및 카드사는 웹페이지 탐색 허용 여부 설정이 필요합니다. `WKNavigationDelegate` protocol 중 아래 메서드를 구현해야 합니다.

```swift
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
    decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let url = navigationAction.request.url, url.scheme != "http" && url.scheme != "https" {
        UIApplication.shared.open(url, options: [:], completionHandler: { (success) in
            if !(success) {
                /* 앱이 설치되어 있지 않을 때 */
            }
        })
        decisionHandler(.cancel)
    } else {
        decisionHandler(.allow)
    }
}
```

#### 4-2. AOS (Kotlin)

* Android 5.0 이상
* WebView를 이용한 하이브리드앱이며, 각 PG사에서 제공하는 모바일앱 결제 연동 가이드를 참고해 주세요.
* Manifest.xml 설정, WebViewClient 구현은 5-3. AOS Native 방식과 동일하게 적용합니다.
* 카드사앱 결제 후 자동으로 원래 앱으로 돌아오려면 reservation data에 `inAppYn`, `extraData`를 설정해야 합니다. (5-2 참고)

### 5. Native 방식 연동

#### 5-1. 준비하기 (Native 공통)

**5-1-1. 결제편의 모듈 호출을 위한 스크립트 호출용 html**

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="description" content="">
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <script type="text/javascript" src="https://shop-api.e-ncp.com/payments/ncp_pay.js"></script>
    <script type="text/javascript" src="https://nsp.pay.naver.com/sdk/js/naverpay.min.js"></script>
    <script type="text/javascript" src="https://spay.kcp.co.kr/plugin/kcp_spay_hub.js"></script>
    <script type="text/javascript" src="https://xpay.tosspayments.com/xpay/js/xpay_crossplatform.js"></script>
    <title>ShopbyPayment</title>
</head>
<body>
</body>
</html>
```

필요에 따라 각 PG사에서 제공하는 javascript를 import 해야 합니다.

```html
<script type="text/javascript" src="https://shop-api.e-ncp.com/payments/ncp_pay.js"></script>
<script type="text/javascript" src="https://nsp.pay.naver.com/sdk/js/naverpay.min.js"></script>
<script type="text/javascript" src="https://spay.kcp.co.kr/plugin/kcp_spay_hub.js"></script>
<script type="text/javascript" src="https://web.nicepay.co.kr/v3/webstd/js/nicepay-3.0.js"></script>
<script type="text/javascript" src="https://pay.billgate.net/paygate/plugin/gx_web_client.js"></script>
<script type="text/javascript" src="https://xpay.tosspayments.com/xpay/js/xpay_crossplatform.js"></script>
<script type="text/javascript" src="https://chai.finance/js/v1/payment.min.js"></script>
<script type="text/javascript" src="http://wcs.naver.net/wcslog.js"></script>
<script type="text/javascript" src="https://api.eximbay.com/v1/javascriptSDK.js"></script>
```

**5-1-2. 결제편의 모듈 호출을 위한 configuration parameter**

```javascript
NCPPay.setConfiguration({
    'clientId': ncp.clientId,          // shopby에서 발급받은 clientId
    'confirmUrl': 'scheme://confirm.html', // 결제 완료 후 리턴을 받을 업체의 URL
    'platform': 'IOS',                 // 'IOS' 또는 'AOS'
    'accessToken': '액세스토큰',        // 로그인한 사람의 accessToken
    'currency': 'KRW',                 // 엑심베이 결제 시 포함
    'language': 'KO'                   // 엑심베이 결제 시 포함
});
```

* `accessToken`이 없는 경우 비회원 주문 결제 진행이 됩니다.

**5-1-3. reservation에 필요한 data**

```javascript
NCPPay.reservation(data, function (response) {
    alert("결제완료 되었습니다.");
});
```

전달되어야 하는 request 값을 json 형태로 `data` 파라미터에 전달합니다. ([결제 예약 데이터 양식](https://docs.shopby.co.kr/?url.primaryName=order/#/Purchase/post-payments-reserve) 참고)

#### 5-2. iOS (Swift)

* iOS12 이상
* WebView를 이용하여 샵바이결제편의(ncppay) 모듈을 호출합니다.

**결제편의 모듈의 Reservation 값 설정**

reservation data 중 `inAppYn`, `extraData`값을 같이 설정해야 카드사앱 결제 후 자동으로 다시 원래 앱으로 돌아옵니다.

```
"inAppYn": "Y",
"extraData": {
    "appUrl": "scheme://" // 앱스키마명
},
```

```javascript
NCPPay.reservation(data, function (response) {
    alert("결제완료 되었습니다.");
});
```

**info.plist 파일에 딥링크(URL Scheme) 설정**

```xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>앱식별자</string> <!-- 번들명(bundle identifier) -->
        <key>CFBundleURLSchemes</key>
        <array>
            <string>앱스키마명</string> <!-- 앱스키마명(scheme://) -->
        </array>
    </dict>
</array>
```

**쿠키 설정**

```swift
func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    HTTPCookieStorage.shared.cookieAcceptPolicy = .always
    ...
}

// 또는
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions) {
    HTTPCookieStorage.shared.cookieAcceptPolicy = .always
    ...
}
```

**구현하기 — 샵바이결제편의모듈(ncppay) 호출 방식 정하기**

**1) local에서 호출하기 — html File로 호출하기**

웹뷰(WebView)에서 local html 파일을 load 합니다. (준비하기 5-1-1을 참고하여 예: `ncppay_module.html` 파일을 만들어 호출)

```swift
guard let filePath = Bundle.main.path(forResource: "ncppay_module", ofType: "html") else { return }
let url = URL(fileURLWithPath: filePath)
let request = URLRequest(url: url)
wkWebView.load(request)
```

웹뷰(WebView) `evaluateJavaScript`를 이용해 NCPPay 모듈의 `setConfiguration`, `reservation` 함수를 호출합니다. (준비하기 5-1-2, 5-1-3 참고)

```swift
let injectJavaScript: String = """
NCPPay.setConfiguration({
    'clientId': '\(API_CLIENT_ID)',
    'confirmUrl': '앱스키마명://confirm.html',
    'platform': 'IOS',
    'accessToken': '\(accessToken)',
    'currency': 'KRW', // 엑심베이 결제 시 포함
    'language': 'KO'   // 엑심베이 결제 시 포함
});
const obj = JSON.parse("\(data)");
NCPPay.reservation(obj, function (response) {
});
"""

var jsStr = injectJavaScript.trimmingCharacters(in: .whitespaces)
jsStr = jsStr.trimmingCharacters(in: .whitespacesAndNewlines)
jsStr = jsStr.trimmingCharacters(in: .newlines)
wkWebView.evaluateJavaScript(jsStr, completionHandler: { result, error in
    if let result = result {
        print("injected evaluateJavaScript JS: \(result)")
    }
})
```

**2) local에서 호출하기 — htmlString으로 호출하기**

setConfiguration, reservation 함수 호출에 필요한 데이터가 준비되면 아래처럼 htmlString을 만들어 호출합니다. (준비하기 5-1-1, 5-1-2, 5-1-3 참고)

```swift
let htmlString: String = """
<script type="text/javascript" src="https://shop-api.e-ncp.com/payments/ncp_pay.js"></script>
<script type="text/javascript" src="https://nsp.pay.naver.com/sdk/js/naverpay.min.js"></script>
<script type="text/javascript" src="https://spay.kcp.co.kr/plugin/kcp_spay_hub.js"></script>
<script type="text/javascript" src="https://xpayvvip.uplus.co.kr/xpay/js/xpay_crossplatform.js"></script>
// 기타 필요한 PG사 스크립트를 import 합니다.
<title>ShopbyPayment</title>
<script>
NCPPay.setConfiguration({
    'clientId': '\(API_CLIENT_ID)',
    'confirmUrl': '앱스키마명://comfirm.html',
    'platform': 'IOS',
    'accessToken': '\(accessToken)',
    'currency': 'KRW',
    'language': 'KO'
});
const obj = JSON.parse("\(data)");
NCPPay.reservation(obj, function (response) {
});
</script>
"""
wkWebView.loadHTMLString(htmlString, baseURL: Bundle.main.bundleURL)
```

**3) remote(url)로 호출하기**

방식은 위 "local html File 호출하기"와 동일합니다.

```swift
let url = URL(string: "https://도메인.com/ncppay_module.html")
let request = URLRequest(url: url!)
wkWebView.load(request)
```

**결제 결과 confirmUrl 처리하기**

`NCPPay.setConfiguration`에 설정한 `confirmUrl`로 성공/실패의 결과값을 리턴받습니다.

```
Uri: /confirmhtml?orderSheetNo=주문번호&guestToken(비회원주문)=토큰&PayType=CREDIT_CARD&result=(SUCCESS 또는 FAIL)
```

* 회원 주문인 경우 `guestToken`은 제외됩니다.
* `guestToken`은 비회원 주문 상세 조회 API 등 비회원 관련 구현이 필요한 곳에 사용됩니다.
* **성공인 경우**: `result=SUCCESS`와 함께 주문번호를 전달합니다. (예: `result=SUCCESS&orderNo=123`)
* **실패인 경우**: `result=FAIL`과 함께 실패 사유를 `message`에 담아 전달합니다. (예: `result=FAIL&message=[01]+PG사+결제+실패하였습니다`)

웹뷰 설정(딥링크: 앱간 이동 처리 포함):

```swift
// iOS 12
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
    decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
    var policy = WKNavigationActionPolicy.allow
    if let requestURL = navigationAction.request.url {
        if (requestURL.host == "itunes.apple.com" || requestURL.scheme == "tel"
            || requestURL.scheme == "sms" || requestURL.scheme == "mailto")
            && UIApplication.shared.canOpenURL(requestURL) {
            UIApplication.shared.open(requestURL, options: [:], completionHandler: nil)
            policy = .cancel
        } else if requestURL.scheme != "http" && requestURL.scheme != "https" {
            if requestURL.scheme == "file" {
                policy = .allow
            } else {
                // confirmUrl로 설정한 '앱스키마명://comfirm.html'을 통해 앱간 이동으로 결제 결과값이 전송됩니다.
                if requestURL.scheme == "앱스키마명" {
                    if let host = requestURL.host, host == "confirm.html" {
                        if let _ = requestURL.query as String? {
                            returnConfirmUrl(requestURL: requestURL)
                        }
                        policy = .allow
                        decisionHandler(policy)
                        return
                    }
                }
                UIApplication.shared.open(requestURL, options: [:], completionHandler: nil)
                policy = .cancel
            }
        }
    }
    decisionHandler(policy)
}

private func returnConfirmUrl(requestURL: URL) {
    if let urlComponent = URLComponents(url: requestURL, resolvingAgainstBaseURL: true) {
        let queryItems = urlComponent.queryItems
        let successValue = queryItems?.first(where: { $0.name == "result" })?.value
        if successValue == "SUCCESS" {
            guard let orderNo = queryItems?.first(where: { $0.name == "orderNo" })?.value else { return }
            // 성공 프로세스 구현
        } else if successValue == "FAIL" {
            guard let message = queryItems?.first(where: { $0.name == "message" })?.value else { return }
            // 실패 프로세스 구현
        }
    }
}
```

**주의사항**

**> 토스(허브형) : local에서 샵바이결제편의모듈(ncppay) 호출 시 ATS 설정**

local에서 샵바이결제편의모듈을 호출하고 토스에서 제공하는 javascript를 호출하는 경우, 토스 javascript 내에서 http를 사용하고 있기 때문에 ATS 설정을 하지 않으면 팝업이 노출되지 않습니다. (remote 호출 시에는 정상 노출)

```xml
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
```

**> 네이버페이(간편결제) : local에서 샵바이결제편의모듈(ncppay) 호출 시**

`pgType=NAVER_EASY_PAY`이고 local에서 호출하는 경우, 팝업창을 노출하기 위해 WKWebView의 `allowFileAccessFromFileURLs`를 true로 설정합니다. (remote 호출 시에는 정상 노출)

```swift
wkWebView.configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
```

#### 5-3. AOS (Kotlin)

* Android 5.0 이상
* WebView를 이용하여 샵바이결제편의(ncppay) 모듈을 호출합니다.
* 결제 App별 패키지명은 3-1을 참고하세요.

**웹뷰 사용을 위한 Manifest.xml 설정**

웹뷰 사용을 위해 인터넷 퍼미션과 `usesCleartextTraffic`을 설정합니다.

```xml
<uses-permission android:name="android.permission.INTERNET" />

<application
    ...
    android:usesCleartextTraffic="true"
    ...>
    ...
</application>
```

**종속 항목 추가**

웹뷰 AssetLoader를 사용하기 위한 웹킷을 앱 수준의 `build.gradle`에 종속 항목으로 추가합니다.

```groovy
dependencies {
    implementation "androidx.webkit:webkit:1.4.0"
}
```

**구현하기**

**5-3-1. WebViewClient 구현**

`WebViewClient`를 상속받은 클래스를 구현합니다. `shouldOverrideUrlLoading`에는 들어오는 url에 대해 호스트가 처리할지, WebView에서 load할지 결정합니다. url은 다양한 scheme 형태로 들어올 수 있기 때문에 예외 처리가 필요합니다.

```kotlin
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
    view ?: return false
    url ?: return false
    if (url.isBlank()) return false

    // scheme url 형태인지 판별해야한다.
    if (!(url.startsWith("http") || url.startsWith("https"))) {
        try {
            return when {
                url.startsWith("intent://") -> urlIntent(view.context, url)
                url.startsWith("market://") -> urlMarket(view.context, url)
                url.startsWith("ncppay://") -> urlNcpPay(url)
                else -> {
                    return try {
                        view.context.startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(url) })
                        true
                    } catch (e: Exception) {
                        urlIntent(view.context, url)
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    } else {
        view.loadUrl(url)
    }
    return true
}
```

**5-3-2. 결제 결과 confirmUrl 처리하기**

`NCPPay.setConfiguration`에 설정한 confirmUrl로 성공/실패의 결과값을 리턴받습니다. (URI 형식·success/fail 규칙은 5-2의 confirmUrl 처리하기와 동일)

```kotlin
// if (url.startWith("ncppay://")
private fun urlNcpPay(url: String): Boolean {
    val uri = Uri.parse(url)
    val params: Set<String> = uri.queryParameterNames
    if (params.contains("result")) {
        val result = uri.getQueryParameter("result") ?: ""
        val orderNo = uri.getQueryParameter("orderNo") ?: "알 수 없음"
        when (result) {
            RESULT_SUCCESS_DEFAULT -> {
                // result = SUCCESS 일 때 처리
                return true
            }
            RESULT_FAILED_DEFAULT -> {
                // result = FAIL 일 때 처리
                return true
            }
        }
    }
    return false
}
```

**5-3-3. Android 11(API 30) 패키지 가시성 제한**

api 30 이상을 타겟팅하는 경우, 패키지 가시성 정책이 변경되어 특정 사유가 있지 않는 한 `QUERY_ALL_PACKAGES`의 사용이 제한됩니다. 대응 방안으로 AndroidManifest.xml에 패키지를 정의하거나, `ActivityNotFoundException`을 캐치하여 앱 설치 여부를 체크해야 합니다.

*AndroidManifest.xml에 패키지 정의* (카드사별 패키지명은 3-1 참고)

```xml
<queries>
    <package android:name="com.nhnent.payapp" /> <!-- 페이코 -->
    <package android:name="com.lottemembers.android" /> <!-- LPAY -->
    <package android:name="com.ssg.serviceapp.android.egiftcertificate" /> <!-- SSGPAY -->
    <!-- .... -->
</queries>
```

*ActivityNotFoundException 캐치*

`startActivity()` 호출 시 `ActivityNotFoundException`이 발생한다면 앱이 설치되어 있지 않은 경우이므로 마켓으로 이동하여 설치를 유도해야 합니다.

```kotlin
private fun urlIntent(context: Context, url: String): Boolean {
    val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
    val intentPackageName = intent.getPackage()
    try {
        context.startActivity(intent)
        return true
    } catch (e: ActivityNotFoundException) {
        val builder = AlertDialog.Builder(context).apply {
            setTitle(context.getText(R.string.app_install_title))
            setMessage(context.getText(R.string.app_install_message))
            setPositiveButton(context.getText(R.string.confirm)) { _, _ ->
                val marketIntent = Intent(Intent.ACTION_VIEW)
                marketIntent.data = Uri.parse("market://details?id=$intentPackageName")
                try {
                    context.startActivity(marketIntent)
                } catch (e: ActivityNotFoundException) { }
            }
            setCancelable(false)
        }
        builder.create().show()
        return true
    } catch (e: Exception) {
        e.printStackTrace()
        context.startActivity(intent)
        return true
    }
}
```

**5-3-4. 페이지 상태 처리**

페이지 상태(Start, Finish, Error)에 따라 액티비티에서 처리하려면 리스너를 이용합니다. 페이지가 Finished 되면 액티비티에서 NCPPay 관련 함수를 호출해야 하기 때문에 각 상태에 따라 리스너를 호출합니다.

```kotlin
class MyWebViewClient : WebViewClient() {
    private var mListener: NCMWebListener? = null
    fun setWebListener(listener: NCMWebListener) {
        mListener = listener
    }
    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        mListener?.onPageStarted(url, favicon)
    }
    override fun onPageFinished(view: WebView?, url: String?) {
        mListener?.onPageFinished(url)
    }
}

interface NCMWebListener {
    fun onPageStarted(url: String?, favicon: Bitmap?) {}
    fun onPageFinished(url: String?) {}
    fun onPageError(errorCode: Int, description: String?, failingUrl: String?) {}
}
```

**5-3-5. WebView 액티비티 코드 작성**

```kotlin
class PgWebViewActivity : AppCompatActivity(), NCMWebListener, PayResultListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityPgWebviewBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.webview.apply {
            webViewClient = NCMWebViewClient(applicationContext).apply {
                setWebListener(this@PgWebViewActivity)
                setPayResultListener(this@PgWebViewActivity)
            }
            webChromeClient = WebChromeClient()
            settings.apply {
                javaScriptCanOpenWindowsAutomatically = true
                javaScriptEnabled = true
                setSupportMultipleWindows(true)
                loadWithOverviewMode = true
                useWideViewPort = true
                cacheMode = WebSettings.LOAD_DEFAULT
            }
        }
    }
}
```

**5-3-6. 결제 Url 로딩**

```kotlin
webview.loadUrl("https://도메인.com/NCPPay_ALPHA.html") // 간편결제 Url 로딩
```

**5-3-7. 리스너 오버라이딩 – WebListener**

리스너를 통해 상태를 전달받기 위해 WebListener 함수를 오버라이딩합니다. 웹 상태가 Finish가 되면 NCPPay를 자바스크립트로 호출합니다.

```kotlin
class PgWebViewActivity : AppCompatActivity(), NCMWebListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        webViewClient = NCMWebViewClient(applicationContext).apply {
            setWebListener(this@PgWebViewActivity)
        }
    }
    override fun onPageStarted(url: String?, favicon: Bitmap?) {
        super.onPageStarted(url, favicon)
        binding.progressBar2.isVisible = true
    }
    override fun onPageFinished(url: String?) {
        super.onPageFinished(url)
        if (url == Constants.SHOPBY_PAYMENT_URL) {
            executePaymentProcess(
                clientId = Constants.TEST_CLIENT_ID,
                platform = "AOS",
                confirmUrl = "ncppay://confirm.html",
                data = Gson().toJson(MainApplication.orderRequestData),
                accessToken = userAccessToken
            )
        }
        binding.progressBar2.isVisible = false
    }
}

fun executePaymentProcess(clientId: String, confirmUrl: String, platform: String, data: String, accessToken: String? = null) {
    setScript("""
        NCPPay.setConfiguration({
            'clientId': "$clientId",
            'confirmUrl': '$confirmUrl',
            'platform': '$platform',
            'accessToken': '$accessToken',
            'currency': 'KRW',
            'language': 'KO'
        });
        NCPPay.reservation(JSON.parse(`$data`), function (response) {
            alert("결제완료 되었습니다.");
        });
    """.trimIndent())
}

private fun setScript(script: String, callback: ValueCallback<String>? = null) {
    binding.webview.evaluateJavascript(script, callback)
}
```

> 자바스크립트 호출부에 대한 자세한 사항은 [PC 결제 연동 가이드](https://shopby.works/guide/dev-cover/order#pay-button)를 참고하세요.

**5-3-8. AssetLoader를 통한 로컬 html 파일 직접 읽어오기**

프로젝트 내부 html asset을 불러올 때, 안드로이드에서는 보안상의 이유로 `file://` 형태의 url 사용을 지양합니다. Android API 30(11)부터 `allowFileAccessFromFileURLs`가 deprecated 되었으며, 대신 `AssetLoader`를 통해 html 에셋에 접근하는 것을 권장합니다.

```kotlin
class MyWebViewClient(context: Context) : WebViewClient() {
    val assetLoader = WebViewAssetLoader.Builder()
        .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context))
        .build()

    @RequiresApi(24)
    override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest): WebResourceResponse? {
        return assetLoader.shouldInterceptRequest(request.url)
    }

    @SuppressWarnings("deprecation")
    override fun shouldInterceptRequest(view: WebView?, request: String?): WebResourceResponse? {
        return assetLoader.shouldInterceptRequest(Uri.parse(request))
    }
}
```

웹뷰가 속한 액티비티에서 다음과 같이 로드하면 assets 폴더 내의 html 파일을 안전하게 불러올 수 있습니다. (아래는 assets 폴더 안의 index.html을 호출하는 예시)

```kotlin
binding.webView.loadUrl("https://appassets.androidplatform.net/assets/index.html")
```

**5-3-9. String 타입의 HTML 코드 불러오기**

```kotlin
const val SHOPBY_PAYMENT_HTML = """
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <script type="text/javascript" src="https://shop-api.e-ncp.com/payments/ncp_pay.js"></script>
        <script type="text/javascript" src="https://nsp.pay.naver.com/sdk/js/naverpay.min.js"></script>
        <script type="text/javascript" src="https://spay.kcp.co.kr/plugin/kcp_spay_hub.js"></script>
        <title>ShopbyPayment</title>
    </head>
    <body>
    </body>
    </html>
"""
```

위와 같은 String 타입의 HTML 코드를 불러오려면 WebView의 `loadDataWithBaseURL` 속성을 이용합니다. BaseUrl 미설정 시 `about:blank`로 로드되므로, `https://`로 시작하는 임의의 url을 인자로 넣습니다.

```kotlin
webView.loadDataWithBaseURL(
    baseUrl = SHOPBY_PAYMENT_BASE,
    data = SHOPBY_PAYMENT_HTML,
    mimeType = "text/html; charset=utf-8",
    encoding = "utf-8",
    historyUrl = null
)
```

### 6. 샘플 프로젝트 다운로드

제공된 샘플은 샵바이 API와 샵바이결제편의 모듈을 연동해 구동하기 위한 최소한의 프로젝트입니다. 개발 환경이나 앱 개발 상황에 따라 구현 방식이 달라질 수 있으니 참고용으로 사용해 주세요.

{% file src="/files/BvYCJ6KBL7czlIsbXcoU" %}

{% file src="/files/blVJykPhTGBwenKS8hEu" %}
