> ## Documentation Index
> Fetch the complete documentation index at: https://developers.argosidentity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# LiveForm WebView 설치 가이드

> 고객사 모바일 앱(iOS·Android·React Native)에 LiveForm을 WebView로 연동하는 방법

이 문서는 고객사 모바일 앱에 LiveForm을 WebView로 연동하는 방법을 설명합니다.

**대상 독자:** 기존 iOS 또는 Android 앱에 LiveForm을 연동하는 고객사 모바일 엔지니어.

## 구현 목표

고객사 앱은 다음 흐름을 구현합니다.

1. 고객사 서버 또는 앱에서 전달받은 `pid`로 LiveForm URL을 생성합니다.
2. WebView를 열기 전에 LiveForm에 필요한 앱 권한을 확인합니다.
3. LiveForm URL을 전체 화면 WebView로 엽니다.
4. 카메라 촬영, 이미지 업로드, 쿠키, 외부 링크 이동이 동작하도록 WebView를 설정합니다.

## 지원 및 최소 사양

LiveForm은 HTTPS 기반 Web Application이므로 앱의 WebView가 최신 브라우저 기능, 카메라 권한, 파일 선택, cookie/storage를 정상 지원해야 합니다.

| 구분              | 최소 사양                                     | 권장 사양                               | 비고                                                                                          |
| --------------- | ----------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------- |
| iOS             | iOS 15 이상                                 | iOS 16 이상                           | iOS 15 이상에서는 `WKUIDelegate`의 media capture permission callback으로 LiveForm host만 허용할 수 있습니다. |
| Android         | Android 9, API 28 이상                      | Android 10, API 29 이상               | 파일 선택과 카메라 capture 동작 안정성을 위해 Android System WebView 또는 Chrome 최신 버전 사용을 권장합니다.             |
| Android WebView | 업데이트 가능한 Android System WebView 또는 Chrome | 최신 Android System WebView 또는 Chrome | 파일 선택, 카메라 capture, cookie/storage 동작이 WebView/Chrome 버전에 영향을 받을 수 있습니다.                    |
| React Native    | `react-native-webview` 13.x 이상            | 최신 안정 버전                            | 네이티브 앱은 동일 기능을 `WKWebView`/Android `WebView`로 직접 구현하면 됩니다.                                  |

실기기 검수에서 확인해야 하는 범위:

* Android 9(API 28) 이상에서 앱 설치, WebView 로딩, 파일 선택, 카메라 capture, cookie/storage를 포함한 전체 흐름을 확인합니다.
* iOS 15 이상에서 `WKWebView` 파일 선택, 카메라 capture, cookie/storage를 포함한 전체 흐름을 확인합니다.
* 최종 운영 판정은 실제 LiveForm URL에서 촬영, 업로드, 제출, returnUrl/deep link 이동까지 완료되는지 보고 내려야 합니다.

## LiveForm URL

고객사 앱은 LiveForm 운영 host만 사용합니다. `pid`는 query parameter로 전달합니다.

```
https://form.argosidentity.com?pid={pid}
```

<Warning>
  `pid`는 URL에 넣기 전에 반드시 URL encode 처리해야 합니다.
</Warning>

고객사 앱에서는 별도의 host나 form type path를 구분하지 않고, 전달받은 `pid`로만 LiveForm 세션을 구분합니다.

## 앱 권한

LiveForm은 WebView 내부에서 카메라와 이미지 업로드 기능을 사용합니다. 모바일 앱에서 WebView로 실행하는 경우, 브라우저가 처리하던 카메라와 파일 접근 권한을 고객사 앱이 먼저 확보해야 합니다.

<Warning>
  WebView를 열기 전에 앱 레벨에서 카메라와 사진첩 또는 미디어 권한을 요청하고, 허용 상태를 확인하는 것은 필수입니다. 필수 권한이 허용되지 않은 상태에서는 LiveForm의 촬영 또는 이미지 업로드 흐름이 정상 동작하지 않을 수 있으므로 WebView를 열지 않아야 합니다.
</Warning>

<Tabs>
  <Tab title="iOS">
    `Info.plist`에 다음 usage description을 추가합니다.

    ```xml theme={null}
    <key>NSCameraUsageDescription</key>
    <string>LiveForm 신분증 촬영을 위해 카메라 접근이 필요합니다.</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>LiveForm 이미지 업로드를 위해 사진첩 접근이 필요합니다.</string>
    ```
  </Tab>

  <Tab title="Android">
    `AndroidManifest.xml`에 다음 권한을 추가합니다.

    ```xml theme={null}
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    ```

    <Note>
      이미지 업로드를 지원하는 경우, 고객사 앱의 target SDK와 파일 선택 방식에 맞는 Android 사진/미디어 권한도 함께 설정해야 합니다.
    </Note>
  </Tab>
</Tabs>

## 필수 연동 흐름

```
사용자 액션
  └─ 고객사 앱에서 LiveForm 시작
      └─ pid 기반 LiveForm URL 생성

WebView 진입 전
  ├─ 카메라 권한 확인
  ├─ 사진첩/미디어 권한 확인
  └─ 전체 화면 WebView 열기

WebView 내부
  ├─ JavaScript와 DOM storage 허용
  ├─ Cookie 허용
  ├─ inline media와 media capture 허용
  ├─ http/https/data/about URL은 WebView에서 처리
  └─ custom scheme은 외부 앱으로 전달
```

URL이 유효하지 않거나 필수 권한 확인이 끝나지 않았다면 WebView를 열지 않습니다.

## WebView 설정

WebView는 JavaScript, storage, cookie, media playback, media capture를 허용해야 합니다.

<Tabs>
  <Tab title="React Native">
    React Native 앱에서 `react-native-webview`를 사용하는 경우, Android `WebChromeClient`와 iOS `WKUIDelegate`의 세부 구현은 라이브러리 native layer가 처리합니다. 앱 코드에서는 아래 WebView props를 설정하고, 실제 기기에서 Android/iOS 검수 체크리스트를 각각 확인해야 합니다.

    ```tsx theme={null}
    import { Linking } from 'react-native';
    import { WebView } from 'react-native-webview';

    function openExternalUrl(url: string) {
      Linking.canOpenURL(url)
        .then((supported) => {
          if (supported) return Linking.openURL(url);
          return undefined;
        })
        .catch(() => undefined);
    }

    export function LiveFormWebView({ url }: { url: string }) {
      return (
        <WebView
          source={{ uri: url }}
          originWhitelist={['*']}
          javaScriptEnabled
          domStorageEnabled
          sharedCookiesEnabled
          thirdPartyCookiesEnabled
          allowsInlineMediaPlayback
          mediaPlaybackRequiresUserAction={false}
          mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"
          allowsFullscreenVideo
          allowFileAccess
          mixedContentMode="compatibility"
          setSupportMultipleWindows={false}
          onOpenWindow={({ nativeEvent }) => {
            if (nativeEvent.targetUrl) openExternalUrl(nativeEvent.targetUrl);
          }}
          onShouldStartLoadWithRequest={({ url: nextUrl }) => {
            if (
              nextUrl === 'about:blank' ||
              nextUrl.startsWith('about:') ||
              nextUrl.startsWith('data:') ||
              nextUrl.startsWith('http://') ||
              nextUrl.startsWith('https://')
            ) {
              return true;
            }

            openExternalUrl(nextUrl);
            return false;
          }}
        />
      );
    }
    ```

    **주요 설정 설명**

    | 설정                                                  | 이유                                                  |
    | --------------------------------------------------- | --------------------------------------------------- |
    | `javaScriptEnabled`                                 | LiveForm은 Web Application으로 실행됩니다.                  |
    | `domStorageEnabled`                                 | LiveForm이 브라우저 storage를 사용할 수 있게 합니다.               |
    | `sharedCookiesEnabled` / `thirdPartyCookiesEnabled` | 일반 브라우저와 유사한 cookie 동작을 유지합니다.                      |
    | `allowsInlineMediaPlayback`                         | 지원되는 환경에서 media UI가 WebView 내부에서 동작하도록 합니다.         |
    | `mediaCapturePermissionGrantType`                   | LiveForm host에서 media capture 권한을 처리할 수 있게 합니다.     |
    | `setSupportMultipleWindows={false}`                 | 새 창 흐름을 앱이 제어하는 WebView 경로 안에 둡니다.                  |
    | `onShouldStartLoadWithRequest`                      | 알 수 없는 custom scheme이 WebView를 깨지 않도록 외부 앱으로 전달합니다. |
  </Tab>

  <Tab title="Android 네이티브">
    Android 네이티브 앱에서는 `WebView.settings`를 설정하고, `WebChromeClient`에서 WebView 내부 카메라 권한 요청과 파일 업로드 요청을 처리합니다.

    ```kotlin theme={null}
    import android.Manifest
    import android.annotation.SuppressLint
    import android.app.Activity
    import android.content.ActivityNotFoundException
    import android.content.Intent
    import android.net.Uri
    import android.os.Bundle
    import android.webkit.CookieManager
    import android.webkit.PermissionRequest
    import android.webkit.ValueCallback
    import android.webkit.WebChromeClient
    import android.webkit.WebChromeClient.FileChooserParams
    import android.webkit.WebResourceRequest
    import android.webkit.WebSettings
    import android.webkit.WebView
    import android.webkit.WebViewClient
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.content.ContextCompat
    import android.content.pm.PackageManager

    class LiveFormActivity : AppCompatActivity() {
        private lateinit var webView: WebView
        private var filePathCallback: ValueCallback<Array<Uri>>? = null

        private val fileChooserLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                val callback = filePathCallback
                filePathCallback = null

                if (callback == null) return@registerForActivityResult
                if (result.resultCode != Activity.RESULT_OK) {
                    callback.onReceiveValue(null)
                    return@registerForActivityResult
                }

                val data = result.data
                val uris = when {
                    data?.clipData != null -> {
                        val clipData = data.clipData!!
                        Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
                    }
                    data?.data != null -> arrayOf(data.data!!)
                    else -> null
                }

                callback.onReceiveValue(uris)
            }

        @SuppressLint("SetJavaScriptEnabled")
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            val liveFormUrl = intent.getStringExtra("LIVE_FORM_URL") ?: return finish()

            webView = WebView(this)
            setContentView(webView)

            CookieManager.getInstance().setAcceptCookie(true)
            CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)

            webView.settings.apply {
                javaScriptEnabled = true
                domStorageEnabled = true
                mediaPlaybackRequiresUserGesture = false
                mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
                allowFileAccess = true
            }

            webView.webViewClient = object : WebViewClient() {
                override fun shouldOverrideUrlLoading(
                    view: WebView,
                    request: WebResourceRequest,
                ): Boolean {
                    val uri = request.url
                    val scheme = uri.scheme

                    if (scheme == "http" || scheme == "https" || scheme == "about" || scheme == "data") {
                        return false
                    }

                    return try {
                        startActivity(Intent(Intent.ACTION_VIEW, uri))
                        true
                    } catch (_: ActivityNotFoundException) {
                        true
                    }
                }
            }

            webView.webChromeClient = object : WebChromeClient() {
                override fun onPermissionRequest(request: PermissionRequest) {
                    val origin = request.origin.host ?: return request.deny()
                    val allowedHost = origin == "form.argosidentity.com"

                    val cameraGranted = ContextCompat.checkSelfPermission(
                        this@LiveFormActivity,
                        Manifest.permission.CAMERA,
                    ) == PackageManager.PERMISSION_GRANTED

                    val resources = request.resources.filter {
                        it == PermissionRequest.RESOURCE_VIDEO_CAPTURE
                    }.toTypedArray()

                    if (allowedHost && cameraGranted && resources.isNotEmpty()) {
                        request.grant(resources)
                    } else {
                        request.deny()
                    }
                }

                override fun onShowFileChooser(
                    webView: WebView,
                    filePathCallback: ValueCallback<Array<Uri>>,
                    fileChooserParams: FileChooserParams,
                ): Boolean {
                    this@LiveFormActivity.filePathCallback?.onReceiveValue(null)
                    this@LiveFormActivity.filePathCallback = filePathCallback

                    return try {
                        fileChooserLauncher.launch(fileChooserParams.createIntent())
                        true
                    } catch (_: ActivityNotFoundException) {
                        this@LiveFormActivity.filePathCallback = null
                        filePathCallback.onReceiveValue(null)
                        false
                    }
                }
            }

            webView.loadUrl(liveFormUrl)
        }
    }
    ```

    **Android 필수 사항**

    * `CAMERA` 권한은 WebView를 열기 전에 앱에서 먼저 요청하고 허용 상태를 확인합니다.
    * Android 13 이상에서 이미지 선택 권한이 필요한 구조라면 고객사 파일 선택 구현에 맞춰 `READ_MEDIA_IMAGES` 등을 처리합니다.
    * `onPermissionRequest`에서는 LiveForm host만 허용합니다. 모든 origin에 camera 권한을 grant하지 마세요.
    * `onShowFileChooser`를 구현해야 `<input type="file">` 기반 업로드가 안정적으로 동작합니다.
  </Tab>

  <Tab title="iOS 네이티브">
    iOS 네이티브 앱에서는 `WKWebView`를 사용하고, JavaScript와 inline media playback을 허용합니다. iOS 15 이상에서는 `WKUIDelegate`의 media capture permission callback에서 LiveForm host만 허용할 수 있습니다.

    ```swift theme={null}
    import UIKit
    import WebKit
    import AVFoundation
    import Photos

    final class LiveFormViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
        private var webView: WKWebView!
        private let liveFormURL: URL

        init(liveFormURL: URL) {
            self.liveFormURL = liveFormURL
            super.init(nibName: nil, bundle: nil)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func viewDidLoad() {
            super.viewDidLoad()

            let configuration = WKWebViewConfiguration()
            configuration.allowsInlineMediaPlayback = true
            configuration.defaultWebpagePreferences.allowsContentJavaScript = true
            configuration.websiteDataStore = .default()

            webView = WKWebView(frame: .zero, configuration: configuration)
            webView.navigationDelegate = self
            webView.uiDelegate = self
            webView.translatesAutoresizingMaskIntoConstraints = false

            view.addSubview(webView)
            NSLayoutConstraint.activate([
                webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
                webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
                webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            ])

            webView.load(URLRequest(url: liveFormURL))
        }

        func webView(
            _ webView: WKWebView,
            decidePolicyFor navigationAction: WKNavigationAction,
            decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
        ) {
            guard let url = navigationAction.request.url else {
                decisionHandler(.cancel)
                return
            }

            if let scheme = url.scheme, ["http", "https", "about", "data"].contains(scheme) {
                decisionHandler(.allow)
                return
            }

            UIApplication.shared.open(url)
            decisionHandler(.cancel)
        }

        @available(iOS 15.0, *)
        func webView(
            _ webView: WKWebView,
            requestMediaCapturePermissionFor origin: WKSecurityOrigin,
            initiatedByFrame frame: WKFrameInfo,
            type: WKMediaCaptureType,
            decisionHandler: @escaping (WKPermissionDecision) -> Void
        ) {
            let allowedHost = origin.host == "form.argosidentity.com"

            decisionHandler(allowedHost ? .grant : .deny)
        }
    }
    ```

    **iOS 필수 사항**

    * `NSCameraUsageDescription`과 `NSPhotoLibraryUsageDescription`을 반드시 설정합니다.
    * WebView를 열기 전에 `AVCaptureDevice.requestAccess(for: .video)` 등으로 카메라 권한을 먼저 요청하고 허용 상태를 확인합니다.
    * 이미지 업로드 흐름을 사용하는 경우 Photo Library 접근 정책을 고객사 앱 UX에 맞게 확인합니다.
    * media capture permission은 LiveForm host만 허용합니다.
  </Tab>
</Tabs>

## LiveForm URL 생성 유틸리티

```tsx theme={null}
const LIVE_FORM_ORIGIN = 'https://form.argosidentity.com';

export function buildLiveFormUrl(pid: string) {
  return `${LIVE_FORM_ORIGIN}?pid=${encodeURIComponent(pid.trim())}`;
}
```

## 권한 처리 예시 (Expo/React Native)

```tsx theme={null}
import * as ImagePicker from 'expo-image-picker';

export async function requestLiveFormPermissions() {
  const camera = await ImagePicker.requestCameraPermissionsAsync();
  const mediaLibrary = await ImagePicker.requestMediaLibraryPermissionsAsync();

  return {
    granted: camera.granted && mediaLibrary.granted,
    camera,
    mediaLibrary,
  };
}
```

권한이 거부되면:

* WebView를 열지 않습니다.
* LiveForm 촬영과 업로드에 카메라/사진첩 권한이 필요하다는 메시지를 보여줍니다.
* 앱 설정 화면으로 이동할 수 있는 버튼을 제공합니다.

## 전체 화면 레이아웃

LiveForm은 가능한 많은 화면 영역을 사용하는 것이 좋습니다.

권장 사항:

* WebView route에서는 native header를 숨깁니다.
* notch와 home indicator를 피하기 위해 safe area를 적용합니다.
* header를 숨긴 경우 작은 앱 레벨 뒤로가기 버튼을 제공합니다.
* WebView를 card나 modal 안에 넣지 말고 화면을 직접 채우게 합니다.

```
┌──────────────────────────┐
│  back                    │
│ ┌──────────────────────┐ │
│ │                      │ │
│ │      LiveForm        │ │
│ │      WebView         │ │
│ │                      │ │
│ └──────────────────────┘ │
└──────────────────────────┘
```

## 검수 체크리스트

iOS와 Android에서 각각 확인합니다.

<Info>
  **검수 방식**

  고객사 앱은 제공된 예시 구현을 참고해 자체 WebView 화면을 구현한 뒤, 실제 LiveForm URL과 권한/기능 테스트 페이지를 분리해서 검수해야 합니다.

  * **LiveForm URL 모드:** LiveForm 운영 호스트에서 실제 제출 흐름을 확인합니다.
  * **권한/기능 테스트 페이지:** WebView의 storage/cookie, fetch, 파일 입력, 카메라 capture 입력을 LiveForm 상태와 무관하게 한 번의 흐름으로 순차 확인합니다.

  권한/기능 테스트 성공은 WebView 메커니즘이 동작한다는 뜻이지, 실제 LiveForm 제출 성공을 의미하지 않습니다. 최종 판정은 LiveForm URL 모드와 권한/기능 테스트 모드를 모두 보고 내려야 합니다.
</Info>

<Tabs>
  <Tab title="공통 (URL·권한)">
    **URL 및 이동**

    * `https://form.argosidentity.com?pid={pid}` 형식의 LiveForm URL이 열린다.
    * `pid`가 URL encode 처리되어 전달된다.
    * 외부 custom scheme은 외부 앱으로 열리거나 안전하게 실패한다.

    **권한**

    * 첫 실행 시 WebView 진입 전에 카메라 권한을 확인한다.
    * 첫 실행 시 WebView 진입 전에 사진첩/미디어 권한을 확인한다.
    * 권한 거부 시 복구 가능한 메시지를 보여준다.
    * 설정 열기 버튼이 앱 설정 화면으로 이동한다.

    **WebView 동작**

    * JavaScript가 활성화된 상태로 LiveForm이 로드된다.
    * 신분증 촬영 시 카메라 UI가 열린다.
    * 이미지 업로드 시 picker가 열린다.
    * Cookie와 storage가 일반 WebView navigation 동안 유지된다.
    * 페이지 로딩 중 loading state가 표시된다.
    * WebView load error 또는 HTTP error가 발생했을 때 사용자에게 복구 가능한 안내를 보여준다.
  </Tab>

  <Tab title="Android">
    * `AndroidManifest.xml`에 `android.permission.CAMERA`가 선언되어 있다.
    * WebView를 열기 전에 Android runtime camera permission을 요청하고 허용 상태를 확인한다.
    * Native WebView 구현에서는 `WebChromeClient.onPermissionRequest`를 구현한다.
    * Native WebView 구현에서는 `WebChromeClient.onShowFileChooser`를 구현한다.
    * Android System WebView 또는 Chrome이 최신 버전인지 확인한다.
    * `javaScriptEnabled`가 활성화되어 있다.
    * `domStorageEnabled`가 활성화되어 있다.
    * Cookie와 third-party cookie가 허용되어 있다.
    * media playback 또는 camera access가 사용자 gesture 정책이나 WebView 설정으로 차단되지 않는다.
    * 파일 선택 성공, 취소, 실패 모든 경로에서 `ValueCallback`이 정확히 한 번 호출된다.
    * 파일 업로드 결과가 `content://` URI로 전달되어도 정상 처리된다.
    * 고객사 앱의 파일 선택 구현이 `file://` URI를 직접 노출하지 않는다.
    * LiveForm의 파일 선택 UI에서 `accept` 속성에 맞는 파일 타입이 선택 가능하다.
    * LiveForm의 촬영 UI에서 `capture` 속성에 맞게 카메라 촬영 경로가 열린다.
    * HTTPS 정책 또는 mixed content 정책으로 LiveForm 리소스가 차단되지 않는다.
    * `returnUrl` 또는 deep link 이동이 외부 앱/브라우저/고객사 앱으로 정상 전달된다.
  </Tab>

  <Tab title="iOS WKWebView">
    * `Info.plist`에 `NSCameraUsageDescription`이 선언되어 있다.
    * `Info.plist`에 `NSPhotoLibraryUsageDescription`이 선언되어 있다.
    * WebView를 열기 전에 앱의 카메라 권한 요청과 허용 상태 확인이 정상 동작한다.
    * WebView를 열기 전에 앱의 사진첩 권한 요청과 허용 상태 확인이 정상 동작한다.
    * Native WKWebView 구현에서는 `WKUIDelegate`가 설정되어 있다.
    * iOS 15 이상에서는 `WKUIDelegate`의 media capture permission 처리를 확인한다.
    * `WKWebView`에서 `input type="file"`이 정상 동작한다.
    * 파일 선택 또는 카메라 촬영 후 선택된 파일이 WebView로 정상 전달된다.
    * LiveForm의 파일 선택 UI에서 `accept` 속성에 맞는 파일 타입이 선택 가능하다.
    * LiveForm의 촬영 UI에서 `capture` 속성에 맞게 카메라 촬영 경로가 열린다.
    * HEIC 이미지 업로드가 서버 또는 WebView 흐름에서 정상 처리된다.
    * JavaScript 실행이 활성화되어 있다.
    * `window.open` 또는 새 창 요청이 차단되지 않고 앱 정책에 맞게 처리된다.
    * `WKWebView`의 cookie와 website data store 정책이 LiveForm 흐름을 차단하지 않는다.
    * HTTPS 정책 또는 mixed content 정책으로 LiveForm 리소스가 차단되지 않는다.
    * `returnUrl`, universal link, deep link 이동이 고객사 앱 정책에 맞게 정상 처리된다.
  </Tab>

  <Tab title="React Native">
    * `react-native-webview`의 `javaScriptEnabled`가 활성화되어 있다.
    * `react-native-webview`의 `domStorageEnabled`가 활성화되어 있다.
    * `sharedCookiesEnabled`와 `thirdPartyCookiesEnabled`가 앱 정책에 맞게 설정되어 있다.
    * media capture 관련 WebView prop이 카메라 접근을 차단하지 않는다.
    * `allowFileAccess`와 파일 선택 흐름이 Android/iOS 모두에서 정상 동작한다.
    * Expo/RN permission API가 카메라와 사진첩/미디어 권한 상태를 정확히 반환한다.
    * `onShouldStartLoadWithRequest`, `onOpenWindow`, `Linking.openURL` 조합이 returnUrl/deep link를 안전하게 처리한다.
    * `onError`와 `onHttpError`가 사용자에게 복구 가능한 안내와 진단 리포트를 제공한다.
  </Tab>
</Tabs>

## 문제 해결

<AccordionGroup>
  <Accordion title="LiveForm 안에서 카메라가 열리지 않음">
    확인할 항목:

    * WebView를 열기 전에 앱 카메라 권한을 요청했고 허용되었는지
    * iOS `NSCameraUsageDescription`이 설정되어 있는지
    * Android `android.permission.CAMERA`가 설정되어 있는지
    * WebView에서 media capture 관련 설정이 활성화되어 있는지

    <Tip>
      인앱 브라우저(웹뷰) 환경에서 카메라 차단이 지속되는 경우, [Android·웹뷰 카메라 문제 해결](/ko/idcheck/getting-started/webview-camera-troubleshooting)을 참고하세요.
    </Tip>
  </Accordion>

  <Accordion title="이미지 업로드가 동작하지 않음">
    확인할 항목:

    * 사진첩 또는 미디어 권한을 요청했고 허용되었는지
    * iOS `NSPhotoLibraryUsageDescription`이 설정되어 있는지
    * Android media 권한이 target SDK와 picker 구현에 맞게 설정되어 있는지
  </Accordion>

  <Accordion title="WebView가 빈 화면으로 보임">
    확인할 항목:

    * URL에 유효한 `pid`가 포함되어 있는지
    * 기기가 네트워크에 연결되어 있는지
    * JavaScript와 DOM storage가 활성화되어 있는지
    * WebView callback에서 HTTP/network error가 발생했는지
  </Accordion>
</AccordionGroup>
