# 오퍼월 광고

## 기본 요건 <a href="#basics" id="basics"></a>

[시작 가이드](/nestads-sdk-dev/android/readme/dev.md)에 따라 필요한 과정을 완료합니다.

**추가 요구사항:**

* NestAdsSDK 버전 v2.6.4 - NestAdsOfferwallSDK v0.1.0
* NestAdsSDK 버전 v2.6.5 이후 출시 버전 - NestAdsOfferwallSDK v1.0.0

{% hint style="warning" %}
**중요**: Offerwall 광고를 사용하기 위해서 minSdk 24 이상이 요구됩니다.
{% endhint %}

***

## 설치 <a href="#installation" id="installation"></a>

1. 프로젝트 단 `settings.gradle` 파일에 아래의 오퍼월 광고를 위한 maven repository를 추가합니다.

{% tabs %}
{% tab title="Gradle" %}

```gradle
dependencyResolutionManagement {
   repositories {
      mavenCentral()

       // JitPack repository for AdChain SDK
      maven { url = uri("https://jitpack.io") }

      // Adjoe SDK Maven repository
      maven { url = uri("https://releases.adjoe.io/maven") }
   }
}
```

{% endtab %}
{% endtabs %}

2. `app` 모듈 수준의 `build.gradle` 파일에 `offerwall sdk` 의존성을 추가합니다.

{% tabs %}
{% tab title="Gradle" %}

```gradle
dependencies {
   implementation 'com.nestads:nestads-offerwall-sdk-android:$version'
}
```

{% endtab %}
{% endtabs %}

***

## 테스트 광고 게재위치 <a href="#test-placement-code" id="test-placement-code"></a>

Placement Code(게재위치 코드)는 어드민의 인벤토리 > 게재위치 메뉴에서 게재위치를 등록 시 시스템을 통해 자동으로 생성됩니다.

***

## Offerwall 통합하기 <a href="#integration" id="integration"></a>

### 1. Application 클래스에서 초기화

Offerwall SDK를 사용하려면 Application 클래스에서 `OfferwallConfig`를 통해 초기화해야 합니다.

{% hint style="warning" %}
**중요**: Application 클래스에서 반드시 `import com.nestads.sdk.offerwall.model.OfferwallConfig`를 추가해야 합니다. 이것이 없으면 런타임에 SDK를 찾을 수 없습니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import android.app.Application
import com.nestads.sdk.ads.NestAds
import com.nestads.sdk.offerwall.model.OfferwallConfig  // ⚠️ 필수!

class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()

        // 1. Offerwall 설정 생성
        val offerwallConfig = OfferwallConfig.Builder(
            appKey = APP_KEY,        // 앱 키
            appSecret = APP_SECRET   // 앱 시크릿
        )
            .setTimeoutMillis(30000)  // 선택사항: 타임아웃 설정 (밀리초)
            .build()

        // 2. NestAds 초기화 (Offerwall 설정 포함)
        NestAds.initialize(this, offerwallConfig)
    }

    companion object {
        private const val APP_KEY = "YOUR_APP_KEY"
        private const val APP_SECRET = "YOUR_APP_SECRET"
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
`appKey`, `appSecret` 은 발급 과정이 필요합니다. 담당자에게 문의 부탁드립니다.
{% endhint %}

### 초기화 상태 확인

SDK 초기화는 백그라운드에서 비동기로 진행됩니다. 초기화 완료 여부는 아래 속성으로 확인할 수 있습니다:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
if (NestAds.Offerwall.isInitialized) {
    // 초기화 완료 - 로그인 및 다른 기능 사용 가능
    Log.d("NestAds", "Offerwall SDK 초기화 완료")
} else {
    // 아직 초기화 안 됨 - 서버 검증 진행 중
    Log.d("NestAds", "Offerwall SDK 초기화 진행 중...")
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**참고**: `initialize()` 호출 직후에는 `false`일 수 있습니다. 일반적으로 앱 시작 후 1-2초 내에 완료됩니다.
{% endhint %}

### 2. AndroidManifest.xml에 Application 클래스 등록

Application 클래스를 생성한 후에는 반드시 `AndroidManifest.xml`에 등록해야 합니다.

```xml
<application
    android:name=".MyApp"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">

    <!-- 나머지 설정 -->

</application>
```

{% hint style="warning" %}
`android:name=".MyApp"` 속성을 추가하지 않으면 Application 클래스가 실행되지 않아 SDK가 초기화되지 않습니다.
{% endhint %}

***

### 사용자 로그인

Offerwall을 표시하기 전에 반드시 사용자 로그인을 완료해야 합니다.

### 사용자 정보와 함께 로그인

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
NestAds.Offerwall.login(
    userId, // 필수
    gender, // 선택
    birthYear, // 선택
    customProperties // 선택
)
```

{% endtab %}
{% endtabs %}

***

{% hint style="info" %}
**파라미터 설명:**

* `userId` - **\[필수]** 사용자 고유 ID
* `gender` - **\[선택]** 성별.
  * `NestAdsGenderMale` (1) - 남성
  * `NestAdsGenderFemale` (2) - 여성
* `birthYear` - **\[선택]** 출생 연도. 타겟 광고 최적화에 활용됩니다.
  {% endhint %}

***

### 로그인 리스너 사용

```kotlin
val user = OfferwallUser(userId = "user123")

val loginListener = object : OfferwallLoginListener {
    override fun onSuccess() {
        Log.d("NestAds", "로그인 성공")
        Toast.makeText(context, "로그인되었습니다", Toast.LENGTH_SHORT).show()
    }

    override fun onFailure(errorType: OfferwallLoginListener.ErrorType) {
        Log.e("NestAds", "로그인 실패: $errorType")
        Toast.makeText(context, "로그인 실패", Toast.LENGTH_SHORT).show()
    }
}

NestAds.Offerwall.login(user, loginListener)
```

***

### 로그인 에러 타입별 처리

```kotlin
val loginListener = object : OfferwallLoginListener {
    override fun onSuccess() {
        Log.d("NestAds", "로그인 성공")
        // 메인 화면으로 이동
        startMainActivity()
    }

    override fun onFailure(errorType: OfferwallLoginListener.ErrorType) {
        when (errorType) {
            OfferwallLoginListener.ErrorType.NOT_INITIALIZED -> {
                Log.e("NestAds", "SDK가 초기화되지 않았습니다")
            }
            OfferwallLoginListener.ErrorType.INVALID_USER_ID -> {
                Log.e("NestAds", "잘못된 사용자 ID")
            }
            OfferwallLoginListener.ErrorType.NETWORK_ERROR -> {
                Log.e("NestAds", "네트워크 오류")
            }
            OfferwallLoginListener.ErrorType.AUTHENTICATION_FAILED -> {
                Log.e("NestAds", "인증 실패")
            }
            OfferwallLoginListener.ErrorType.UNKNOWN -> {
                Log.e("NestAds", "알 수 없는 오류")
            }
        }
    }
}

```

***

### 로그인 상태 확인

사용자가 로그인되어 있는지 확인할 수 있습니다. Offerwall을 표시하기 전에 로그인 상태를 확인하는 것이 좋습니다:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
if (NestAds.Offerwall.isLoggedIn) {
    // 로그인 상태 - Offerwall 사용 가능
    Log.d("NestAds", "사용자 로그인 상태")
} else {
    // 로그인 필요 - 로그인 화면으로 이동
    Log.d("NestAds", "로그인이 필요합니다")
    showLoginScreen()
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Offerwall을 표시하거나 Quiz/Mission 기능을 사용하기 전에 반드시 로그인이 완료되어 있어야 합니다.
{% endhint %}

***

### 3. Offerwall 표시하기 <a href="#display" id="display"></a>

로그인 완료 후 원하는 시점에 Offerwall을 표시할 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
fun openOfferwall(
  context: Context,
  placementCode: String,
  callback: OfferwallCallback? = null,
  eventCallback: OfferwallEventCallback? = null
)

// 사용 예시
NestAds.Offerwall.openOfferwall(
    context = context,
    placementCode = "YOUR_PLACEMENT_CODE",
    callback = object : OfferwallCallback {
        ...
    },
    eventCallback = object : OfferwallEventCallback {
        ...
    }
)
```

{% endtab %}
{% endtabs %}

## 로그아웃 <a href="#logout" id="logout"></a>

사용자가 로그아웃할 때 Offerwall에서도 로그아웃 처리를 해야 합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
NestAds.Offerwall.logout()
```

{% endtab %}
{% endtabs %}

***

## 상태 확인 <a href="#status" id="status"></a>

### 현재 사용자 정보 조회

로그인된 사용자의 ID를 확인할 수 있습니다:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val userId = NestAds.Offerwall.currentUserId

if (userId != null) {
    Log.d("NestAds", "현재 로그인된 사용자: $userId")
    // 사용자 ID를 활용한 로직
    displayUserInfo(userId)
} else {
    Log.d("NestAds", "로그인된 사용자가 없습니다")
}
```

{% endtab %}
{% endtabs %}

***

### SDK 버전 정보

현재 사용 중인 SDK 버전을 확인할 수 있습니다:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val sdkVersion = NestAds.Offerwall.sdkVersion
val adchainSDKVersion = NestAds.Offerwall.adchainSdkVersion

Log.d("NestAds", "Offerwall SDK 버전: $sdkVersion")
Log.d("NestAds", "AdChain SDK 버전: $adchainSDKVersion")
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
SDK 버전 정보는 디버깅이나 고객 지원 시 유용하게 활용할 수 있습니다.
{% endhint %}

***

### 종합 활용 예시

Offerwall을 안전하게 표시하기 위한 전체 흐름 예시입니다:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
fun showOfferwallSafely() {
    // 1. 초기화 상태 확인
    if (!NestAds.Offerwall.isInitialized) {
        Log.e("NestAds", "Offerwall이 초기화되지 않았습니다")
        Toast.makeText(this, "SDK 초기화 중입니다. 잠시 후 다시 시도해주세요.", Toast.LENGTH_SHORT).show()
        return
    }

    // 2. 로그인 상태 확인
    if (!NestAds.Offerwall.isLoggedIn) {
        Log.e("NestAds", "로그인이 필요합니다")
        Toast.makeText(this, "로그인이 필요합니다.", Toast.LENGTH_SHORT).show()
        // 로그인 화면으로 이동
        startLoginActivity()
        return
    }

    // 3. 사용자 정보 로깅
    val userId = NestAds.Offerwall.currentUserId
    Log.d("NestAds", "Offerwall 표시 - 사용자: $userId")

    // 4. Offerwall 표시
    NestAds.Offerwall.openOfferwall(
        context = this,
        placementCode = "YOUR_PLACEMENT_CODE",
        callback = object : OfferwallCallback {
            override fun onOfferwallOpened() {
                Log.d("NestAds", "Offerwall 열림")
            }

            override fun onOfferwallClosed() {
                Log.d("NestAds", "Offerwall 닫힘")
            }

            override fun onOfferwallFailed(error: OfferwallAdError) {
                Log.e("NestAds", "Offerwall 실패: ${error.message}")
            }
        }
    )
}
```

{% endtab %}
{% endtabs %}

***

## Quiz 광고 <a href="#quiz" id="quiz"></a>

퀴즈는 사용자가 간단한 질문에 답하고 포인트를 받을 수 있는 기능입니다. 오퍼월과 함께 사용하면 참여율이 높아집니다.

<figure><img src="/files/6efJkxnTZGNetKlQHGhp" alt=""><figcaption></figcaption></figure>

### 필수 설정

Quiz를 사용하기 위해서는 먼저 Offerwall을 초기화하고 사용자 로그인을 완료해야 합니다.

#### 1. Offerwall 초기화 (Application)

{% hint style="info" %}
이미 Offerwall을 설정했다면 이 단계는 생략하셔도 됩니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import android.app.Application
import com.nestads.sdk.offerwall.core.NestAdsOfferwall
import com.nestads.sdk.offerwall.model.OfferwallConfig

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        // Offerwall 설정 생성
        val offerwallConfig = OfferwallConfig.Builder(
            appKey = "YOUR_APP_KEY",
            appSecret = "YOUR_APP_SECRET"
        )
            .setTimeout(30000) // 타임아웃 설정 (밀리초)
            .build()

        // NestAds Offerwall 초기화
        NestAdsOfferwall.initialize(this, offerwallConfig)
    }
}
```

{% endtab %}
{% endtabs %}

#### 2. 사용자 로그인

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import com.nestads.sdk.offerwall.core.NestAdsOfferwall
import com.nestads.sdk.offerwall.model.OfferwallUser
import com.nestads.sdk.offerwall.listener.OfferwallLoginListener

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 사용자 생성 (Quiz 사용 전 필수)
        val user = OfferwallUser.Builder("user_12345").build()

        // 또는 추가 정보 포함
        // val user = OfferwallUser.Builder("user_12345")
        //     .setGender(OfferwallUser.Gender.MALE)
        //     .setBirthYear(1990)
        //     .build()

        // 로그인
        NestAdsOfferwall.login(user, object : OfferwallLoginListener {
            override fun onSuccess() {
                // 로그인 성공
            }

            override fun onFailure(errorType: OfferwallLoginListener.ErrorType) {
                // 로그인 실패
            }
        })
    }
}
```

{% endtab %}
{% endtabs %}

***

### Quiz 사용하기

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import com.nestads.sdk.offerwall.core.NestAds
import com.nestads.sdk.offerwall.listener.OfferwallQuizEventsListener
import com.nestads.sdk.offerwall.model.quiz.OfferwallQuizResponse
import com.nestads.sdk.offerwall.model.quiz.OfferwallQuizEvent
import com.nestads.sdk.offerwall.model.OfferwallAdError

class QuizActivity : AppCompatActivity() {

    private val quiz = NestAds.Offerwall.quiz()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_quiz)

        setupQuiz()
    }

    private fun setupQuiz() {
        // 1. Quiz 이벤트 리스너 설정
        quiz.setQuizEventsListener { quizEvent, rewardAmount ->
            // Quiz 완료
            println("✅ Quiz 완료!")
            println("획득 포인트: $rewardAmount")
            println("Quiz ID: ${quizEvent.id}")

            // 리워드 지급 로직
            UserManager.addPoints(rewardAmount)
        }

        // 2. Quiz 데이터 로드
        loadQuizList()
    }

    private fun loadQuizList() {
        quiz.getQuizList(
            onSuccess = { response ->
                // Quiz 로드 성공
                println("✅ Quiz 로드 성공")
                println("이벤트 개수: ${response.events.size}")

                // response.events에서 사용 가능한 Quiz 목록 확인
                response.events.forEach { event ->
                    println("Quiz ID: ${event.id}")
                    println("Quiz Title: ${event.title}")
                }
            },
            onFailure = { error ->
                // Quiz 로드 실패
                println("❌ Quiz 로드 실패")
                println("Error: [${error.code}] ${error.message}")
            }
        )
    }

    fun startQuiz(view: View) {
        // Quiz 실행 (Quiz ID를 사용하여 특정 Quiz 시작)
        quiz.clickQuiz("quiz_id_123")
    }
}
```

{% endtab %}
{% endtabs %}

***

### 전체 구현 예제

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.nestads.sdk.offerwall.core.NestAds
import com.nestads.sdk.offerwall.listener.OfferwallQuizEventsListener
import com.nestads.sdk.offerwall.model.OfferwallAdError
import com.nestads.sdk.offerwall.model.quiz.OfferwallQuizEvent
import com.nestads.sdk.offerwall.model.quiz.OfferwallQuizResponse

class QuizActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var loadingView: View
    private lateinit var completedBannerView: View

    private val quiz = NestAds.Offerwall.quiz()
    private val quizAdapter = QuizAdapter { quizEvent ->
        onQuizItemClicked(quizEvent)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_quiz)

        setupViews()
        setupQuiz()
    }

    private fun setupViews() {
        recyclerView = findViewById(R.id.recyclerView)
        loadingView = findViewById(R.id.loadingView)
        completedBannerView = findViewById(R.id.completedBannerView)

        recyclerView.apply {
            layoutManager = LinearLayoutManager(this@QuizActivity)
            adapter = quizAdapter
        }
    }

    private fun setupQuiz() {
        // 1. Quiz 이벤트 리스너 설정 (load 전에!)
        quiz.setQuizEventsListener { quizEvent, rewardAmount ->
            // Quiz 완료
            println("✅ Quiz 완료!")
            println("획득 포인트: $rewardAmount")
            println("Quiz ID: ${quizEvent.id}")

            // 리워드 지급
            UserManager.addPoints(rewardAmount)

            // 축하 메시지
            showRewardDialog(rewardAmount)

            // ⚠️ 중요: 목록 새로고침하여 완료 상태 반영
            loadQuizList()
        }

        // 2. Quiz 데이터 로드
        loadQuizList()
    }

    private fun loadQuizList() {
        showLoading(true)

        quiz.getQuizList(
            onSuccess = { response ->
                showLoading(false)
                handleQuizResponse(response)
            },
            onFailure = { error ->
                showLoading(false)
                handleQuizError(error)
            }
        )
    }

    private fun handleQuizResponse(response: OfferwallQuizResponse) {
        println("✅ Quiz 로드 성공: ${response.events.size}개")

        // 모든 퀴즈 완료 시 완료 배너 표시
        if (response.events.isEmpty()) {
            showCompletedBanner(response)
            return
        }

        // Quiz 목록 표시
        completedBannerView.visibility = View.GONE
        recyclerView.visibility = View.VISIBLE
        quizAdapter.submitList(response.events)
    }

    private fun showCompletedBanner(response: OfferwallQuizResponse) {
        recyclerView.visibility = View.GONE
        completedBannerView.visibility = View.VISIBLE

        // 완료 이미지 로드 (Glide, Coil 등 사용)
        response.completedImageUrl?.let { imageUrl ->
            // TODO: 이미지 로더 라이브러리를 사용하여 이미지 로드
            // Glide.with(this).load(imageUrl).into(completedBannerImageView)
        }
    }

    private fun handleQuizError(error: OfferwallAdError) {
        println("❌ Quiz 로드 실패: [${error.code}] ${error.message}")

        AlertDialog.Builder(this)
            .setTitle("오류")
            .setMessage(error.message)
            .setPositiveButton("재시도") { _, _ ->
                loadQuizList()
            }
            .setNegativeButton("취소", null)
            .show()
    }

    private fun onQuizItemClicked(quizEvent: OfferwallQuizEvent) {
        // 완료된 퀴즈는 클릭 방지
        if (quizEvent.completed == true) {
            Toast.makeText(this, "이미 완료된 퀴즈입니다", Toast.LENGTH_SHORT).show()
            return
        }

        // Quiz 실행
        quiz.clickQuiz(quizEvent.id)
    }

    private fun showLoading(show: Boolean) {
        loadingView.visibility = if (show) View.VISIBLE else View.GONE
    }

    private fun showRewardDialog(amount: Int) {
        AlertDialog.Builder(this)
            .setTitle("🎉 축하합니다!")
            .setMessage("$amount 포인트를 획득했습니다!")
            .setPositiveButton("확인", null)
            .show()
    }

    // RecyclerView Adapter
    private class QuizAdapter(
        private val onItemClick: (OfferwallQuizEvent) -> Unit
    ) : RecyclerView.Adapter<QuizViewHolder>() {

        private var items: List<OfferwallQuizEvent> = emptyList()

        fun submitList(newItems: List<OfferwallQuizEvent>) {
            items = newItems
            notifyDataSetChanged()
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuizViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_quiz, parent, false)
            return QuizViewHolder(view)
        }

        override fun onBindViewHolder(holder: QuizViewHolder, position: Int) {
            val item = items[position]
            holder.bind(item, onItemClick)
        }

        override fun getItemCount() = items.size
    }

    // ViewHolder
    private class QuizViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
        private val pointTextView: TextView = itemView.findViewById(R.id.pointTextView)
        private val thumbnailImageView: ImageView = itemView.findViewById(R.id.thumbnailImageView)
        private val completedBadge: View = itemView.findViewById(R.id.completedBadge)

        fun bind(quizEvent: OfferwallQuizEvent, onItemClick: (OfferwallQuizEvent) -> Unit) {
            titleTextView.text = quizEvent.title
            pointTextView.text = quizEvent.point

            // 썸네일 이미지 로드
            // Glide.with(itemView.context).load(quizEvent.imageUrl).into(thumbnailImageView)

            // 완료된 퀴즈 시각적 구분
            if (quizEvent.completed == true) {
                itemView.alpha = 0.5f
                completedBadge.visibility = View.VISIBLE
                itemView.isEnabled = false
            } else {
                itemView.alpha = 1.0f
                completedBadge.visibility = View.GONE
                itemView.isEnabled = true
                itemView.setOnClickListener { onItemClick(quizEvent) }
            }
        }
    }
}
```

{% endtab %}
{% endtabs %}

***

### 베스트 프랙티스

#### 1. 이벤트 리스너를 먼저 설정한 후 데이터 로드

```kotlin
// ✅ 올바른 순서
val quiz = NestAds.Offerwall.quiz()
quiz.setQuizEventsListener { quizEvent, rewardAmount ->
    // 완료 이벤트 처리
}
quiz.getQuizList(...)  // 그 다음 로드

// ❌ 잘못된 순서 - 이벤트를 놓칠 수 있음
val quiz = NestAds.Offerwall.quiz()
quiz.getQuizList(...)
quiz.setQuizEventsListener { ... }
```

#### 2. Quiz 완료 후 반드시 목록 새로고침

```kotlin
quiz.setQuizEventsListener { quizEvent, rewardAmount ->
    // 리워드 지급
    UserManager.addPoints(rewardAmount)

    // ⚠️ 중요: 반드시 목록 새로고침하여 완료 상태 반영
    quiz.getQuizList(
        onSuccess = { response ->
            // UI 업데이트
            updateQuizList(response.events)
        },
        onFailure = { error ->
            // 에러 처리
        }
    )
}
```

#### 3. 완료된 퀴즈 시각적 구분

```kotlin
fun bindQuizItem(view: View, quizEvent: OfferwallQuizEvent) {
    view.titleTextView.text = quizEvent.title
    view.pointTextView.text = quizEvent.point

    // 완료된 퀴즈는 시각적으로 구분
    if (quizEvent.completed == true) {
        view.alpha = 0.5f
        view.completedBadge.visibility = View.VISIBLE
        view.isEnabled = false
    } else {
        view.alpha = 1.0f
        view.completedBadge.visibility = View.GONE
        view.isEnabled = true
    }
}
```

#### 4. 모든 퀴즈 완료 시 완료 배너 표시

```kotlin
quiz.getQuizList(
    onSuccess = { response ->
        // 모든 퀴즈가 완료되면 events가 비어있고 completedImageUrl이 제공됨
        if (response.events.isEmpty()) {
            response.completedImageUrl?.let { imageUrl ->
                showCompletedBanner(
                    imageUrl = imageUrl,
                    width = response.completedImageWidth,
                    height = response.completedImageHeight
                )
            }
            return@getQuizList
        }

        // 일반 퀴즈 목록 표시
        displayQuizList(response.events)
    },
    onFailure = { error ->
        handleError(error)
    }
)
```

***

### Quiz 응답 모델

`OfferwallQuizResponse`는 다음 정보를 포함합니다:

| 속성                     | 타입                         | 설명          |
| ---------------------- | -------------------------- | ----------- |
| `success`              | `Boolean?`                 | 응답 성공 여부    |
| `events`               | `List<OfferwallQuizEvent>` | Quiz 이벤트 목록 |
| `titleText`            | `String?`                  | 완료 후 표시할 제목 |
| `completedImageUrl`    | `String?`                  | 완료 이미지 URL  |
| `completedImageWidth`  | `Int?`                     | 완료 이미지 너비   |
| `completedImageHeight` | `Int?`                     | 완료 이미지 높이   |
| `message`              | `String?`                  | 응답 메시지      |

`OfferwallQuizEvent`는 다음 정보를 포함합니다:

| 속성                | 타입         | 설명           |
| ----------------- | ---------- | ------------ |
| `id`              | `String`   | Quiz 고유 ID   |
| `title`           | `String`   | Quiz 제목      |
| `description`     | `String?`  | Quiz 설명      |
| `imageUrl`        | `String`   | Quiz 이미지 URL |
| `landingUrl`      | `String`   | 랜딩 페이지 URL   |
| `point`           | `String`   | 보상 포인트       |
| `status`          | `String?`  | Quiz 상태      |
| `completed`       | `Boolean?` | 완료 여부        |
| `impressionOrder` | `Int?`     | 노출 순서        |
| `placementId`     | `String?`  | 배치 ID        |

***

## Mission 광고 <a href="#mission" id="mission"></a>

### 필수 설정

Mission을 사용하기 위해서는 먼저 Offerwall을 초기화하고 사용자 로그인을 완료해야 합니다.

#### 1. Offerwall 초기화 (Application)

위의 Quiz 섹션에서 설명한 Offerwall 초기화를 진행하세요.

#### 2. 사용자 로그인

위의 Quiz 섹션에서 설명한 사용자 로그인을 진행하세요.

***

### Mission 사용하기

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import com.nestads.sdk.offerwall.core.NestAds
import com.nestads.sdk.offerwall.listener.OfferwallMissionEventsListener
import com.nestads.sdk.offerwall.model.mission.Mission
import com.nestads.sdk.offerwall.model.mission.MissionStatus
import com.nestads.sdk.offerwall.model.OfferwallAdError

class MissionActivity : AppCompatActivity() {

    private val mission = NestAds.Offerwall.mission()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mission)

        setupMission()
    }

    private fun setupMission() {
        // 1. Mission 이벤트 리스너 설정
        mission.setEventsListener(object : OfferwallMissionEventsListener {
            override fun onCompleted(mission: Mission) {
                // Mission 완료
                println("✅ Mission 완료!")
                println("Mission: ${mission.title}")
                println("Point: ${mission.point}")

                // 리워드 지급 로직
                UserManager.addPoints(mission.point.toInt())
            }

            override fun onProgressed(mission: Mission) {
                // Mission 진행 중
                println("⏳ Mission 진행 중")
                println("Progress: ${mission.progress} / ${mission.total}")
            }

            override fun onRefreshed(unitId: String?) {
                // Mission 리스트 새로고침
                println("🔄 Mission 리스트 새로고침됨")
                unitId?.let { println("Unit ID: $it") }
            }
        })

        // 2. Mission 데이터 로드
        loadMissionList()

        // 3. Mission 상태 조회
        loadMissionStatus()
    }

    private fun loadMissionList() {
        mission.getMissionList(
            onSuccess = { missions ->
                // Mission 로드 성공
                println("✅ Mission 로드 성공")
                println("Mission 개수: ${missions.size}")

                // Mission 목록 확인
                missions.forEach { m ->
                    println("---")
                    println("ID: ${m.id}")
                    println("Title: ${m.title}")
                    println("Status: ${m.status ?: "unknown"}")
                    println("Point: ${m.point}")
                    println("Type: ${m.type}")
                }
            },
            onFailure = { error ->
                // Mission 로드 실패
                println("❌ Mission 로드 실패")
                println("Error: [${error.code}] ${error.message}")
            }
        )
    }

    private fun loadMissionStatus() {
        mission.getMissionStatus(
            onSuccess = { status ->
                // Mission 상태 조회 성공
                println("✅ Mission 상태 조회 성공")
                println("진행도: ${status.current} / ${status.total}")
                println("완료율: ${if (status.total > 0) status.current * 100 / status.total else 0}%")
                println("완료 여부: ${status.isCompleted}")
                println("보상 청구 가능: ${status.canClaimReward}")
            },
            onFailure = { error ->
                println("❌ Mission 상태 조회 실패")
                println("Error: [${error.code}] ${error.message}")
            }
        )
    }

    fun startMission(view: View) {
        // 첫 번째 Mission 실행
        val response = mission.getOfferwallMissionResponse()
        response?.events?.firstOrNull()?.let { firstMission ->
            mission.clickMission(firstMission.id)
        }
    }

    fun refreshMissionList(view: View) {
        // Mission 리스트 새로고침
        mission.refreshMissionList(unitId = "test_unit")
    }

    fun triggerReward(view: View) {
        // 보상 청구
        mission.clickGetReward()
    }

    override fun onDestroy() {
        super.onDestroy()
        // Mission 리소스 정리는 자동으로 처리됨
    }
}
```

{% endtab %}
{% endtabs %}

***

### Mission 응답 모델

`OfferwallMissionResponse`는 다음 정보를 포함합니다:

| 속성                | 타입              | 설명               |
| ----------------- | --------------- | ---------------- |
| `success`         | `Boolean?`      | 응답 성공 여부         |
| `events`          | `List<Mission>` | Mission 목록       |
| `current`         | `Int`           | 현재 완료된 Mission 수 |
| `total`           | `Int`           | 총 Mission 개수     |
| `rewardUrl`       | `String?`       | 리워드 URL          |
| `titleText`       | `String?`       | 제목 텍스트           |
| `descriptionText` | `String?`       | 설명 텍스트           |
| `bottomText`      | `String?`       | 하단 텍스트           |
| `rewardIconUrl`   | `String?`       | 보상 아이콘 URL       |
| `bottomIconUrl`   | `String?`       | 하단 아이콘 URL       |
| `message`         | `String?`       | 응답 메시지           |

`MissionStatus`는 다음 정보를 포함합니다:

| 속성               | 타입        | 설명               |
| ---------------- | --------- | ---------------- |
| `current`        | `Int`     | 완료된 Mission 수    |
| `total`          | `Int`     | 총 Mission 수      |
| `isCompleted`    | `Boolean` | 모든 Mission 완료 여부 |
| `canClaimReward` | `Boolean` | 보상 청구 가능 여부      |

`Mission`은 다음 정보를 포함합니다:

| 속성                | 타입             | 설명             |
| ----------------- | -------------- | -------------- |
| `id`              | `String`       | Mission 고유 ID  |
| `title`           | `String`       | Mission 제목     |
| `description`     | `String`       | Mission 설명     |
| `point`           | `String`       | 보상 포인트         |
| `status`          | `String?`      | Mission 상태     |
| `progress`        | `Int?`         | 현재 진행도 (0-100) |
| `total`           | `Int?`         | 총 진행도          |
| `type`            | `MissionType?` | Mission 타입     |
| `imageUrl`        | `String`       | 이미지 URL        |
| `landingUrl`      | `String`       | 랜딩 URL         |
| `impressionOrder` | `Int?`         | 노출 순서          |
| `placementId`     | `String?`      | 배치 ID          |

`MissionType`은 다음 타입을 지원합니다:

| 타입                    | 값                       | 설명                     |
| --------------------- | ----------------------- | ---------------------- |
| `NORMAL`              | `"normal"`              | 일반 Mission             |
| `OFFERWALL_PROMOTION` | `"offerwall_promotion"` | Offerwall 프로모션 Mission |

***

### Mission 메서드

| 메서드                                      | 설명                 |
| ---------------------------------------- | ------------------ |
| `setEventsListener(listener)`            | Mission 이벤트 리스너 설정 |
| `getMissionList(onSuccess, onFailure)`   | Mission 목록 조회      |
| `getMissionStatus(onSuccess, onFailure)` | Mission 상태 조회      |
| `getOfferwallMissionResponse()`          | 현재 Mission 응답 조회   |
| `clickMission(missionId)`                | Mission 실행         |
| `clickGetReward()`                       | 보상 청구              |
| `refreshMissionList(unitId)`             | Mission 리스트 새로고침   |

***

## 포인트 연동(S2S)

* 마이비 애드체인을 사용하는 파트너는 서버 간(S2S) 연동을 통해 포인트 적립 및 사용자 정보 조회 기능을 구현해야 합니다.
* 자세한 설명은 [Adchain doc](https://adchain-docs.1self.world/s2s/publisher-integration)을 참고 부탁드립니다.

***

## 관련 레퍼런스 <a href="#reference" id="reference"></a>

* [OfferwallConfig](/nestads-sdk-dev/android/reference/offerwallconfig.md) - Offerwall 초기화 설정
* [NestAds.Offerwall](https://github.com/wisebirds/nestads-sdk-guide/blob/dev/android/reference/nestads-offerwall-api.md) - Offerwall API
* [NestAdsOfferwallQuiz](/nestads-sdk-dev/android/reference/offerwallquiz.md) - Offerwall API
* [NestAdsOfferwallMission](/nestads-sdk-dev/android/reference/offerwallmission.md) - Offerwall API

***


---

# 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://docs.nestads.com/nestads-sdk-dev/android/ad-formats/offerwall.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.
