# 임베디드 오퍼월 통합

앱의 특정 탭에 AdChain SDK를 임베디드 방식으로 통합하는 실무 가이드입니다.

***

## 개요

이 가이드는 앱의 혜택/포인트 탭에 AdChain SDK를 임베디드 방식으로 통합하는 방법을 설명합니다. `<AdchainOfferwallView />` 컴포넌트를 사용하여 기존 화면에 오퍼월을 삽입합니다.

일반적인 SDK 사용법은 [API 레퍼런스](/api/react-native.md)를 참고하세요.

***

## 빠른 시작: 필수 Props

임베디드 오퍼월에서 사용하는 Props와 필수 여부는 다음과 같습니다:

| Props                    | 필수?          | 용도                 |
| ------------------------ | ------------ | ------------------ |
| `placementId`            | ✅ 필수         | 오퍼월 식별자            |
| `style`                  | ✅ 필수         | `{ flex: 1 }` 권장   |
| `onCustomEvent`          | ✅ 필수         | WebView → 앱 이벤트 처리 |
| `onBackPressOnFirstPage` | Android에서 필수 | 백버튼: 첫 페이지일 때      |
| `onBackNavigated`        | Android에서 필수 | 백버튼: 뒤로가기 성공 시     |
| `onOfferwallOpened`      | 선택           | WebView 로드 완료      |
| `onOfferwallClosed`      | 선택           | 오퍼월 닫힘             |
| `onOfferwallError`       | 권장           | WebView 로딩 실패      |

***

## placementId 설정

placementId는 애드체인 팀과 사전 협의하여 정의합니다. 이 예시에서는 `"benefits_tab"`을 사용합니다.

```tsx
<AdchainOfferwallView
  placementId="benefits_tab"  // 실제 값은 애드체인 팀과 협의
  // ...
/>
```

**주의**: 실제 placementId는 애드체인 서버 설정과 일치해야 합니다. 값을 변경할 경우 AdChain 팀과 사전 협의가 필요합니다.

***

## 필수 이벤트 처리: onCustomEvent

임베디드 오퍼월에서 가장 중요한 Props입니다. WebView에서 발생하는 이벤트를 Native로 전달받아 처리합니다.

### 처리해야 하는 이벤트

#### 1. `buy_ticket` (구현 예시)

사용자가 오퍼월에서 특정 액션을 요청할 때 발생합니다. 앱의 해당 기능 UI를 표시합니다.

**payload 구조** (예시):

```typescript
{
  ticketId: string,
  amount: number
}
```

**처리 방법**:

```tsx
onCustomEvent={(eventType, payload) => {
  if (eventType === 'buy_ticket') {
    // 앱의 티켓 구매 UI를 표시
    ShowBuyTicketUI();
    // 또는 payload를 사용하여 처리
    // const { ticketId, amount } = payload;
  }
}}
```

**참고**: 이 이벤트는 특정 구현 사례입니다. 실제 이벤트 타입과 payload 구조는 애드체인 팀과 사전 협의하여 정의합니다.

#### 2. `show_ticket_list` (구현 예시)

사용자가 보유 항목 목록을 보려고 할 때 발생합니다. 앱의 해당 리스트 화면으로 이동합니다.

**payload 구조** (예시):

```typescript
{
  userId: string
}
```

**처리 방법**:

```tsx
onCustomEvent={(eventType, payload) => {
  if (eventType === 'show_ticket_list') {
    // 앱의 티켓 리스트를 표시
    ShowTicketListUI();
  }
}}
```

**참고**: payload를 사용하지 않는 경우도 있습니다. 실제 구현은 비즈니스 로직에 따라 다릅니다.

### 전체 onCustomEvent 예제

```tsx
<AdchainOfferwallView
  placementId="benefits_tab"
  style={{ flex: 1 }}
  onCustomEvent={(eventType, payload) => {
    console.log('[WebView → App]', eventType, payload);

    // 구현 예시 이벤트 처리
    if (eventType === 'buy_ticket') {
      // 티켓 구매 UI 표시
      ShowBuyTicketUI();
    }
    else if (eventType === 'show_ticket_list') {
      // 티켓 리스트 표시
      ShowTicketListUI();
    }
    // 처리되지 않은 이벤트 로깅
    else {
      console.warn('알 수 없는 이벤트:', eventType, payload);
    }
  }}
/>
```

**중요**: 실제 이벤트 타입과 처리 방법은 애드체인 팀과 사전 협의하여 정의하세요. 위 예시는 참고용입니다.

***

## SafeArea 처리

Android와 iOS에서 상태바/노치 영역을 올바르게 처리하기 위해 SafeArea를 적용해야 합니다.

### 문제 상황

* **Android**: 오퍼월이 상태바 영역까지 올라가서 상단이 화면 끝에 붙어 표시됨
* **iOS**: 노치나 Dynamic Island가 있는 기기에서 오퍼월 콘텐츠가 가려질 수 있음

### 해결 방법

SafeArea 라이브러리를 사용하여 오퍼월을 감쌉니다. `react-native-safe-area-context` 또는 다른 SafeArea 관련 라이브러리를 사용할 수 있습니다.

**1. 라이브러리 설치 (예시)**:

```bash
npm install react-native-safe-area-context
```

**2. SafeAreaView 적용**:

```tsx
import { SafeAreaView } from 'react-native-safe-area-context';
import { AdchainOfferwallView } from '@1selfworld/adchain-sdk-core-react-native';

const BenefitsTab = () => {
  return (
    <SafeAreaView style={{ flex: 1 }} edges={['top']}>
      <AdchainOfferwallView
        style={{ flex: 1, width: '100%' }}
        placementId="benefits_tab"
        // ... 다른 props
      />
    </SafeAreaView>
  );
};
```

**edges 옵션 설명**:

* `edges={['top']}`: 상단만 SafeArea 적용
* 하단 탭바가 있는 경우 `bottom`은 제외합니다
* iOS와 Android 모두 자동으로 대응됩니다

**주의사항**:

* SafeAreaView는 오퍼월을 포함하는 **전체 탭 화면**에 적용합니다
* 다른 화면(홈, 설정 등)에는 영향을 주지 않습니다
* iOS 노치/Dynamic Island 기기와 Android 상태바 영역 모두에서 올바르게 동작합니다
* 프로젝트에서 이미 사용 중인 SafeArea 라이브러리가 있다면 그것을 사용해도 무방합니다

***

## Android 백버튼 처리

Android에서는 하드웨어 백버튼을 직접 처리해야 합니다. 그렇지 않으면 WebView 내부 네비게이션을 무시하고 앱이 종료됩니다.

### 문제 상황

* 사용자가 오퍼월 안에서 여러 페이지를 이동함 (예: 메인 → 이벤트 상세 → 참여 화면)
* 백버튼을 누르면 앱이 종료됨 (WebView 뒤로가기 무시)

### 해결 방법

React Native의 `BackHandler`로 백버튼 이벤트를 캐치하고, `UIManager.dispatchViewManagerCommand`로 네이티브에 처리를 위임합니다.

```tsx
import React, { useRef, useEffect, useState } from 'react';
import { BackHandler, findNodeHandle, UIManager } from 'react-native';
import { AdchainOfferwallView } from '@1selfworld/adchain-sdk-core-react-native';

const BenefitsTab = () => {
  const offerwallViewRef = useRef(null);
  const [shouldAllowExit, setShouldAllowExit] = useState(false);

  // Android 백버튼 처리
  useEffect(() => {
    const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
      if (offerwallViewRef.current) {
        const viewId = findNodeHandle(offerwallViewRef.current);
        if (viewId) {
          UIManager.dispatchViewManagerCommand(viewId, 'handleBackPress', []);
          return true; // 앱 종료 방지
        }
      }
      return false;
    });

    return () => backHandler.remove();
  }, []);

  // 앱 종료 처리
  useEffect(() => {
    if (shouldAllowExit) {
      const timer = setTimeout(() => BackHandler.exitApp(), 100);
      return () => clearTimeout(timer);
    }
  }, [shouldAllowExit]);

  return (
    <AdchainOfferwallView
      ref={offerwallViewRef}
      placementId="benefits_tab"
      style={{ flex: 1, width: '100%' }}

      // Android 백버튼 처리
      onBackPressOnFirstPage={() => {
        console.log('첫 페이지 - 앱 종료 허용');
        setShouldAllowExit(true);
      }}
      onBackNavigated={() => {
        console.log('WebView 뒤로가기 성공');
        setShouldAllowExit(false);
      }}

      // 이벤트 처리
      onCustomEvent={(eventType, payload) => {
        if (eventType === 'buy_ticket') {
          ShowBuyTicketUI();
        } else if (eventType === 'show_ticket_list') {
          ShowTicketListUI();
        }
      }}
    />
  );
};
```

**동작 방식**:

1. 백버튼 누름 → `BackHandler` 이벤트 캐치
2. `handleBackPress` 명령을 네이티브로 전송
3. 네이티브 WebView에서 `canGoBack()` 확인:
   * `true` (2+ 페이지) → WebView 내부 뒤로가기 → `onBackNavigated` 호출
   * `false` (첫 페이지) → `onBackPressOnFirstPage` 호출

**앱 종료 처리 커스터마이징**:

위 예시에서는 `BackHandler.exitApp()`으로 앱을 즉시 종료하지만, 기존 앱의 종료 로직으로 변경할 수 있습니다:

```tsx
onBackPressOnFirstPage={() => {
  // 예시 1: 종료 확인 토스트
  Toast.show('한 번 더 누르면 종료됩니다');

  // 예시 2: 종료 확인 팝업
  Alert.alert('앱 종료', '앱을 종료하시겠습니까?', [
    { text: '취소', style: 'cancel' },
    { text: '종료', onPress: () => BackHandler.exitApp() }
  ]);

  // 예시 3: 홈 화면으로 이동
  navigation.navigate('HomeTab');
}}
```

앱마다 종료 동작이 다를 수 있으므로 (즉시 종료, 토스트 표시, 확인 팝업 등) 기존 앱의 백버튼 동작에 맞춰 구현하세요.

**주의**: iOS에서는 백버튼이 없으므로 이 코드가 불필요합니다. Android에서만 작동합니다.

***

## 전체 샘플 코드

임베디드 오퍼월을 구현하는 전체 코드입니다:

```tsx
import React, { useRef, useEffect, useState } from 'react';
import { BackHandler, findNodeHandle, UIManager, Alert, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AdchainOfferwallView } from '@1selfworld/adchain-sdk-core-react-native';

const BenefitsTab = () => {
  const offerwallViewRef = useRef(null);
  const [shouldAllowExit, setShouldAllowExit] = useState(false);

  // Android 백버튼 처리 (Android만)
  useEffect(() => {
    if (Platform.OS !== 'android') return;

    const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
      if (offerwallViewRef.current) {
        const viewId = findNodeHandle(offerwallViewRef.current);
        if (viewId) {
          UIManager.dispatchViewManagerCommand(viewId, 'handleBackPress', []);
          return true; // 앱 종료 방지
        }
      }
      return false;
    });

    return () => backHandler.remove();
  }, []);

  // 앱 종료 처리
  useEffect(() => {
    if (shouldAllowExit) {
      const timer = setTimeout(() => BackHandler.exitApp(), 100);
      return () => clearTimeout(timer);
    }
  }, [shouldAllowExit]);

  return (
    <SafeAreaView style={{ flex: 1 }} edges={['top']}>
      <AdchainOfferwallView
        ref={offerwallViewRef}
        placementId="benefits_tab"
        style={{ flex: 1, width: '100%' }}

        // Android 백버튼 처리
        onBackPressOnFirstPage={() => {
          console.log('첫 페이지 - 앱 종료 허용');
          setShouldAllowExit(true);
        }}
        onBackNavigated={() => {
          console.log('WebView 뒤로가기 성공');
          setShouldAllowExit(false);
        }}

        // 기본 이벤트 (선택)
        onOfferwallOpened={() => console.log('오퍼월 열림')}
        onOfferwallClosed={() => console.log('오퍼월 닫힘')}
        onOfferwallError={(error) => {
          console.error('오퍼월 오류:', error);
          Alert.alert('오류', '오퍼월을 불러올 수 없습니다. 다시 시도해주세요');
        }}

        // WebView 이벤트 브릿지 (필수)
        onCustomEvent={(eventType, payload) => {
          console.log('[WebView → App]', eventType, payload);

          // 필수 이벤트 처리 (구현 예시)
          if (eventType === 'buy_ticket') {
            // 티켓 구매 UI 표시
            ShowBuyTicketUI();
          }
          else if (eventType === 'show_ticket_list') {
            // 티켓 리스트 표시
            ShowTicketListUI();
          }
          // 처리되지 않은 이벤트 로깅
          else {
            console.warn('처리되지 않은 이벤트:', eventType, payload);
          }
        }}
      />
    </SafeAreaView>
  );
};

export default BenefitsTab;
```

***

## 프로덕션 배포 체크리스트

앱을 배포하기 전에 다음 항목들을 확인하세요:

### SDK 설정

* [ ] `app.json`에 프로덕션 appKey/appSecret 설정됨
* [ ] `environment: 'PRODUCTION'` 설정됨
* [ ] 테스트용 credentials가 커밋되지 않았음

### 초기화

* [ ] 앱 시작 시 `AdchainSdk.initialize()` 호출 확인
* [ ] 초기화 실패 시 에러 처리 로직 존재

### 인증

* [ ] 로그인 플로우에서 `AdchainSdk.login()` 호출
* [ ] userId 유효성 검증 (빈 문자열 방지)

### placementId

* [ ] placementId가 애드체인 서버 설정과 일치하는지 확인
* [ ] 실제 환경 URL이 올바르게 로드되는지 테스트

### UI/UX 처리

* [ ] SafeAreaView 적용됨 (iOS/Android 모두)
  * [ ] iOS: 노치/Dynamic Island 기기에서 테스트
  * [ ] Android: 상태바 영역 확인
* [ ] Android 백버튼 처리 구현됨 (Android 필수)

### 이벤트 처리

* [ ] `onCustomEvent` 핸들러 구현됨
  * [ ] 필수 이벤트 처리 (애드체인 팀과 협의된 이벤트)

### 에러 처리

* [ ] 모든 SDK 메서드에 try-catch 적용
* [ ] 사용자에게 명확한 에러 메시지 표시
* [ ] 처리되지 않은 이벤트 로깅 (`console.warn` 추가)

***

## 문제 해결

### 오퍼월이 로딩되지 않아요

**증상**: 탭이 빈 화면으로 표시됨

**확인사항**:

1. SDK가 초기화되었나요?

   ```tsx
   const isReady = await AdchainSdk.isInitialized();
   console.log('SDK 준비:', isReady);
   ```
2. 사용자가 로그인되었나요?

   ```tsx
   const loggedIn = await AdchainSdk.isLoggedIn();
   console.log('로그인 상태:', loggedIn);
   ```
3. `onOfferwallError`에서 에러가 발생했나요?

   ```tsx
   onOfferwallError={(error) => {
     console.error('오류:', error);  // 로그 확인
   }}
   ```

***

### Android 백버튼이 작동하지 않아요

**증상**: 백버튼을 누르면 앱이 종료됨 (WebView 뒤로가기 무시)

**해결**:

1. `ref={offerwallViewRef}` 추가했는지 확인
2. `BackHandler.addEventListener` 등록했는지 확인
3. `UIManager.dispatchViewManagerCommand` 호출 코드 확인

위의 "Android 백버튼 처리" 섹션의 전체 코드를 참고하세요.

***

### 이벤트가 작동하지 않아요

**증상**: WebView에서 버튼을 눌러도 아무 반응이 없음

**해결**:

1. `onCustomEvent` 핸들러가 구현되었는지 확인
2. 애드체인 팀과 협의된 이벤트 타입을 처리하는지 확인
3. 콘솔 로그 확인:

   ```tsx
   onCustomEvent={(eventType, payload) => {
     console.log('[WebView → App]', eventType, payload);  // 이벤트가 들어오는지 확인
     // ...
   }}
   ```

***

### 환경 전환 (STAGING → PRODUCTION)

테스트 환경에서 프로덕션으로 전환할 때:

**1. `app.json` 수정**:

```json
// 개발/테스트
"environment": "STAGING"

// 프로덕션
"environment": "PRODUCTION"
```

**2. 재빌드 필수**:

```bash
# iOS
npx expo prebuild --platform ios --clean
npx expo run:ios

# Android
npx expo prebuild --platform android --clean
npx expo run:android
```

**중요**: 환경 변경 시 반드시 `npx expo prebuild --clean` 실행!

***

## 추가 리소스

* **일반 SDK 사용법**: [API 레퍼런스](/api/react-native.md)
* **설치 가이드**: [시작하기](/undefined/react-native.md)
* **문제 해결**: [문제 해결 가이드](/undefined-6/common-issues.md)
* **FAQ**: [자주 묻는 질문](/undefined-6/faq.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://adchain-doc.1self.world/undefined-5/embedded-offerwall-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
