# 미션

미션은 사용자가 특정 조건을 달성하면 보상을 받는 기능입니다. 데일리 미션, 위클리 미션 등으로 사용자가 매일 앱을 방문하게 만듭니다.

## 시작하기

### 이벤트 리스너 설정 (권장)

미션 완료, 진행, 갱신 이벤트를 받으려면 리스너를 설정합니다.

```typescript
import AdchainSdk, {
  addMissionCompletedListener,
  addMissionProgressedListener,
  addMissionRefreshedListener
} from '@1selfworld/adchain-sdk-core-react-native';

const MISSION_UNIT_ID = 'mission_main_001'; // 대시보드에서 생성한 Unit ID

useEffect(() => {
  // 미션 완료
  const completedSub = addMissionCompletedListener((event) => {
    if (event.unitId === MISSION_UNIT_ID) {
      loadMissionList();
      Alert.alert('미션 완료!', '포인트가 지급되었습니다.');
    }
  });

  // 미션 진행
  const progressedSub = addMissionProgressedListener((event) => {
    if (event.unitId === MISSION_UNIT_ID) {
      loadMissionList();
    }
  });

  // 미션 갱신 (WebView에서 트리거)
  const refreshedSub = addMissionRefreshedListener((event) => {
    if (event.unitId === MISSION_UNIT_ID) {
      loadMissionList();
    }
  });

  return () => {
    completedSub.remove();
    progressedSub.remove();
    refreshedSub.remove();
  };
}, []);
```

## 미션 목록 조회

현재 진행 중인 미션 목록을 가져옵니다.

```typescript
try {
  const response = await AdchainSdk.loadMissionList(MISSION_UNIT_ID);

  const {
    missions,           // 미션 목록
    completedCount,     // 완료된 미션 개수
    totalCount,         // 전체 미션 개수
    canClaimReward      // 보상 수령 가능 여부
  } = response;

  missions.forEach(mission => {
    console.log(`미션: ${mission.title}`);
    console.log(`보상: ${mission.reward}P`);
  });

  setMissionList(missions);
  setProgress({ current: completedCount, total: totalCount });
  setCanClaimReward(canClaimReward);
} catch (error) {
  console.error('미션 조회 실패:', error);
}
```

{% hint style="info" %}
**중요:** 모든 Mission API는 `unitId` 파라미터가 필수입니다. AdChain 대시보드에서 Mission Unit을 생성하고 해당 ID를 사용하세요.
{% endhint %}

## 응답 데이터 구조

### MissionListResponse

`loadMissionList()`가 반환하는 응답 구조입니다.

| 필드                | 타입          | 설명                            |
| ----------------- | ----------- | ----------------------------- |
| `missions`        | `Mission[]` | 미션 목록                         |
| `completedCount`  | `number`    | 완료된 미션 개수                     |
| `totalCount`      | `number`    | 전체 미션 개수                      |
| `canClaimReward`  | `boolean`   | 보상 수령 가능 여부                   |
| `titleText`       | `string?`   | 미션 섹션 제목 (예: "무료 포인트 모으기!")   |
| `descriptionText` | `string?`   | 설명 텍스트                        |
| `bottomText`      | `string?`   | 하단 텍스트 (예: "800만 포인트 받으러 가기") |
| `rewardIconUrl`   | `string?`   | 보상 아이콘 URL                    |
| `bottomIconUrl`   | `string?`   | 하단 아이콘 URL                    |

### Mission

개별 미션 객체의 구조입니다.

| 필드            | 타입         | 설명            |
| ------------- | ---------- | ------------- |
| `id`          | `string`   | 미션 고유 ID      |
| `title`       | `string`   | 미션 제목         |
| `description` | `string`   | 미션 설명         |
| `imageUrl`    | `string`   | 썸네일 이미지 URL   |
| `actionUrl`   | `string`   | 미션 URL        |
| `reward`      | `number`   | 보상 포인트        |
| `type`        | `string`   | 미션 타입 (아래 참조) |
| `isCompleted` | `boolean?` | 완료 여부         |

**MissionType:**

* `normal` — 일반 미션
* `offerwall_promotion` — 오퍼월 프로모션 (클릭 시 오퍼월 열림)

## 미션 클릭 처리

사용자가 미션을 클릭하면 `clickMission()` 메서드를 호출합니다.

```typescript
const handleMissionClick = async (mission: Mission) => {
  try {
    if (mission.type === 'offerwall_promotion') {
      // 오퍼월 프로모션: 오퍼월 열기
      await AdchainSdk.openOfferwall('mission_promo');
    } else {
      // 일반 미션: 미션 WebView 열기
      await AdchainSdk.clickMission(MISSION_UNIT_ID, mission.id);
    }
  } catch (error) {
    console.error('미션 클릭 실패:', error);
  }
};

// 컴포넌트에서
<TouchableOpacity onPress={() => handleMissionClick(mission)}>
  <Text>{mission.title}</Text>
</TouchableOpacity>
```

### clickMission 동작 흐름

`clickMission()` 호출 시 SDK는 자동으로:

1. `mission_clicked` 이벤트 추적
2. 미션 WebView를 전체화면으로 열기
3. 사용자가 미션 완료 시:
   * `mission_completed` 이벤트 추적
   * `addMissionCompletedListener` 콜백 호출
   * 포인트 자동 지급 (서버)
4. 미션 진행 업데이트 시:
   * `mission_progressed` 이벤트 추적
   * `addMissionProgressedListener` 콜백 호출
5. WebView에서 갱신 트리거 시:
   * `addMissionRefreshedListener` 콜백 호출

## 보상 수령

모든 미션을 완료하면 보상을 수령할 수 있습니다.

```typescript
const handleClaimReward = async () => {
  try {
    await AdchainSdk.claimReward(MISSION_UNIT_ID);
    Alert.alert('보상 수령 완료!', '포인트가 지급되었습니다.');
    loadMissionList();
  } catch (error) {
    console.error('보상 수령 실패:', error);
  }
};

<TouchableOpacity
  onPress={handleClaimReward}
  disabled={!canClaimReward}
>
  <Text>보상 받기</Text>
</TouchableOpacity>
```

`canClaimReward`는 `loadMissionList()` 응답에 포함됩니다 — `true`일 때 보상 수령 UI를 표시하세요.

## 이벤트 리스너 상세

### onCompleted

사용자가 미션을 완료했을 때 호출됩니다. **이 시점에서 UI를 새로고침해야 합니다.**

```typescript
const completedSub = addMissionCompletedListener((event) => {
  // event.missionId: 완료된 미션의 ID
  // event.unitId: 완료된 미션의 Unit ID

  if (event.unitId === MISSION_UNIT_ID) {
    loadMissionList();
    Alert.alert('미션 완료!', '포인트가 지급되었습니다.');
  }
});
```

### onProgressed

미션 진행도가 업데이트되었을 때 호출됩니다.

```typescript
const progressedSub = addMissionProgressedListener((event) => {
  if (event.unitId === MISSION_UNIT_ID) {
    loadMissionList();
  }
});
```

### onRefreshed

미션 WebView에서 전체 미션 목록 갱신을 요청할 때 호출됩니다 (예: 보상 수령 후).

```typescript
const refreshedSub = addMissionRefreshedListener((event) => {
  if (event.unitId === MISSION_UNIT_ID) {
    loadMissionList();
  }
});
```

## 자동 추적 이벤트

SDK는 다음 이벤트를 자동으로 추적하여 AdChain 대시보드에 전송합니다.

| 이벤트명                 | 발생 시점               | 페이로드                                                                                  |
| -------------------- | ------------------- | ------------------------------------------------------------------------------------- |
| `mission_impressed`  | 미션 목록 로드 시 (각 미션마다) | `missionId`, `missionTitle`, `impressionOrder`, `placementId`, `userId`               |
| `mission_clicked`    | 미션 클릭 시             | `missionId`, `missionTitle`, `landingUrl`, `impressionOrder`, `placementId`, `userId` |
| `mission_completed`  | 미션 완료 시             | `missionId`, `missionTitle`, `impressionOrder`, `placementId`, `userId`               |
| `mission_progressed` | 미션 진행 업데이트 시        | `missionId`, `missionTitle`, `progress`, `total`, `userId`                            |
| `mission_refreshed`  | 미션 목록 갱신 시          | `unitId`, `userId`                                                                    |

이 이벤트들은 별도로 구현할 필요가 없으며, AdChain 대시보드의 통계에 자동으로 반영됩니다.

## UI 구현 예시

`FlatList`로 미션 목록을 구현하는 전체 예시:

```typescript
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, TouchableOpacity, Image, Alert, StyleSheet } from 'react-native';
import AdchainSdk, {
  addMissionCompletedListener,
  addMissionProgressedListener,
  addMissionRefreshedListener
} from '@1selfworld/adchain-sdk-core-react-native';

const MISSION_UNIT_ID = 'mission_main_001';

const MissionList = () => {
  const [missions, setMissions] = useState([]);
  const [progress, setProgress] = useState({ current: 0, total: 0 });
  const [canClaimReward, setCanClaimReward] = useState(false);

  const loadMissionList = async () => {
    try {
      const response = await AdchainSdk.loadMissionList(MISSION_UNIT_ID);
      setMissions(response.missions);
      setProgress({
        current: response.completedCount,
        total: response.totalCount,
      });
      setCanClaimReward(response.canClaimReward);
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    loadMissionList();

    const completedSub = addMissionCompletedListener((event) => {
      if (event.unitId === MISSION_UNIT_ID) loadMissionList();
    });
    const progressedSub = addMissionProgressedListener((event) => {
      if (event.unitId === MISSION_UNIT_ID) loadMissionList();
    });
    const refreshedSub = addMissionRefreshedListener((event) => {
      if (event.unitId === MISSION_UNIT_ID) loadMissionList();
    });

    return () => {
      completedSub.remove();
      progressedSub.remove();
      refreshedSub.remove();
    };
  }, []);

  const handleMissionClick = async (mission) => {
    if (mission.isCompleted) return;
    try {
      if (mission.type === 'offerwall_promotion') {
        await AdchainSdk.openOfferwall('mission_promo');
      } else {
        await AdchainSdk.clickMission(MISSION_UNIT_ID, mission.id);
      }
    } catch (error) {
      console.error(error);
    }
  };

  const handleClaimReward = async () => {
    try {
      await AdchainSdk.claimReward(MISSION_UNIT_ID);
      Alert.alert('보상 수령 완료!', '포인트가 지급되었습니다.');
      loadMissionList();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.progressContainer}>
        <Text style={styles.progressText}>
          {progress.current}/{progress.total} 미션 완료
        </Text>
      </View>

      <FlatList
        data={missions}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TouchableOpacity
            onPress={() => handleMissionClick(item)}
            disabled={item.isCompleted}
            style={[
              styles.missionItem,
              item.isCompleted && styles.completedItem,
            ]}
          >
            <Image source={{ uri: item.imageUrl }} style={styles.image} />
            <View style={styles.content}>
              <Text style={styles.title}>{item.title}</Text>
              <Text style={styles.description}>{item.description}</Text>
              <Text style={styles.reward}>{item.reward}P</Text>
            </View>
            {item.isCompleted && (
              <View style={styles.completedBadge}>
                <Text style={styles.badgeText}>완료</Text>
              </View>
            )}
          </TouchableOpacity>
        )}
      />

      {canClaimReward && (
        <TouchableOpacity style={styles.rewardButton} onPress={handleClaimReward}>
          <Text style={styles.rewardButtonText}>보상 받기</Text>
        </TouchableOpacity>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
  progressContainer: { padding: 16 },
  progressText: { fontSize: 16, fontWeight: 'bold' },
  missionItem: { flexDirection: 'row', padding: 16 },
  completedItem: { opacity: 0.5 },
  image: { width: 64, height: 64 },
  content: { flex: 1, marginLeft: 12 },
  title: { fontSize: 16, fontWeight: '600' },
  description: { fontSize: 14, color: '#666', marginTop: 4 },
  reward: { fontSize: 14, color: '#FF6B6B', marginTop: 4 },
  completedBadge: { padding: 4 },
  badgeText: { fontSize: 12, color: '#fff' },
  rewardButton: { padding: 16, backgroundColor: '#FF6B6B' },
  rewardButtonText: { color: '#fff', textAlign: 'center', fontSize: 16 },
});
```

## 여러 Mission Unit 운영

같은 앱에서 여러 Mission Unit을 운영할 수 있습니다 (예: 메인 화면, 프로필 화면). 각 Unit별로 다른 미션 세트를 관리합니다.

```typescript
const MAIN_MISSION_UNIT = 'mission_main_001';
const PROFILE_MISSION_UNIT = 'mission_profile_001';

// 메인 화면
const mainMissions = await AdchainSdk.loadMissionList(MAIN_MISSION_UNIT);
await AdchainSdk.clickMission(MAIN_MISSION_UNIT, missionId);
await AdchainSdk.claimReward(MAIN_MISSION_UNIT);

// 프로필 화면
const profileMissions = await AdchainSdk.loadMissionList(PROFILE_MISSION_UNIT);
await AdchainSdk.clickMission(PROFILE_MISSION_UNIT, missionId);
await AdchainSdk.claimReward(PROFILE_MISSION_UNIT);
```

이벤트 리스너에서는 `event.unitId`로 어느 Unit의 이벤트인지 판단합니다.

## 주의사항

* **로그인 필수**: 사용자가 로그인되어 있어야 미션 조회 가능
* **네트워크 필요**: 오프라인 상태에서는 조회 불가
* **이벤트 리스너 우선 설정**: `loadMissionList()` 호출 전에 리스너를 등록하세요
* **`onCompleted`/`onRefreshed`에서 반드시 새로고침**: 그렇지 않으면 UI가 stale 상태로 남습니다

## 다음 단계

* [퀴즈 사용하기](/undefined-2/quiz.md)
* [오퍼월](/undefined-2/offerwall.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-2/mission.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.
