메인 콘텐츠로 건너뛰기

개요

팝업 광고(Popup Ad)는 특정 순간에 화면에 나타나는 광고 포맷입니다. 앱 실행 시, 콘텐츠 로드 완료 시, 특정 이벤트 발생 시 등 원하는 타이밍에 표시할 수 있으며, 사용자가 닫기 버튼을 누르거나 “오늘 하루 보지 않기”를 선택하여 종료할 수 있습니다.

특징

  • 화면 중앙 또는 하단에 표시되는 팝업 형태
  • 이미지 및 동영상 광고 지원
  • 닫기, 딤(배경) 클릭, “오늘 하루 보지 않기” 옵션 제공
  • 배경색, 텍스트 색상 등 커스터마이징 가능
  • 비침투적이면서도 효과적인 광고 경험
개발 환경에서는 테스트 유닛 ID를 사용하세요. 테스트 유닛 ID 섹션을 참고하세요.

구현 단계

팝업 광고는 다음 4단계로 구현합니다:
  1. 초기화 - AdropPopupAd 인스턴스 생성
  2. 리스너 설정 - 광고 및 닫기 이벤트 수신을 위한 리스너 설정
  3. 광고 로드 - 광고 요청 및 수신
  4. 광고 표시 - 화면에 광고 표시

기본 구현

기본 사용법

import io.adrop.ads.popupAd.AdropPopupAd
import io.adrop.ads.popupAd.AdropPopupAdListener
import io.adrop.ads.popupAd.AdropPopupAdCloseListener
import io.adrop.ads.model.AdropErrorCode

class MainActivity : AppCompatActivity() {
    private var popupAd: AdropPopupAd? = null

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

        // 팝업 광고 로드
        loadPopupAd()
    }

    // 1. 광고 초기화 및 로드
    private fun loadPopupAd() {
        popupAd = AdropPopupAd(this, "YOUR_POPUP_UNIT_ID")
        popupAd?.popupAdListener = popupAdListener
        popupAd?.closeListener = popupCloseListener
        popupAd?.load()
    }

    // 2. 광고 표시
    private fun showPopupAd() {
        popupAd?.let { ad ->
            if (ad.isLoaded) {
                ad.show(this)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        popupAd?.destroy()
    }
}

리스너 구현

// 광고 리스너 구현
private val popupAdListener = object : AdropPopupAdListener {
    // 광고 수신 성공 (필수)
    override fun onAdReceived(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 수신 완료")
        // 광고가 준비되면 표시
        showPopupAd()
    }

    // 광고 수신 실패 (필수)
    override fun onAdFailedToReceive(ad: AdropPopupAd, errorCode: AdropErrorCode) {
        Log.d("Adrop", "팝업 광고 수신 실패: $errorCode")
    }

    // 광고 노출 (선택)
    override fun onAdImpression(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 노출")
    }

    // 광고 클릭 (선택)
    override fun onAdClicked(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 클릭")
    }

    // 팝업 광고가 표시되기 직전 (선택)
    override fun onAdWillPresentFullScreen(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 표시 직전")
    }

    // 팝업 광고가 표시된 직후 (선택)
    override fun onAdDidPresentFullScreen(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 표시 완료")
    }

    // 팝업 광고가 닫히기 직전 (선택)
    override fun onAdWillDismissFullScreen(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 닫히기 직전")
    }

    // 팝업 광고가 닫힌 직후 (선택)
    override fun onAdDidDismissFullScreen(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 닫힘")
        // 다음 광고를 미리 로드
        loadPopupAd()
    }

    // 팝업 광고 표시 실패 (선택)
    override fun onAdFailedToShowFullScreen(ad: AdropPopupAd, errorCode: AdropErrorCode) {
        Log.d("Adrop", "팝업 광고 표시 실패: $errorCode")
    }
}

// 닫기 리스너 구현
private val popupCloseListener = object : AdropPopupAdCloseListener {
    // 닫기 버튼 클릭 (선택)
    override fun onClosed(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 닫기 버튼 클릭")
    }

    // 딤(배경) 클릭 (선택)
    override fun onDimClicked(ad: AdropPopupAd) {
        Log.d("Adrop", "팝업 광고 딤 영역 클릭")
    }

    // "오늘 하루 보지 않기" 클릭 (선택)
    override fun onTodayOffClicked(ad: AdropPopupAd) {
        Log.d("Adrop", "오늘 하루 보지 않기 선택")
        // 오늘 날짜를 저장하여 다음 표시를 제어
        val prefs = getSharedPreferences("adrop_prefs", Context.MODE_PRIVATE)
        prefs.edit().putLong("last_popup_hidden_date", System.currentTimeMillis()).apply()
    }
}

커스터마이징

팝업 광고의 외형을 커스터마이징할 수 있습니다.

배경색 및 텍스트 색상 설정

private fun loadPopupAd() {
    popupAd = AdropPopupAd(this, "YOUR_POPUP_UNIT_ID")
    popupAd?.popupAdListener = popupAdListener
    popupAd?.closeListener = popupCloseListener

    // 배경색 설정 (딤 영역)
    popupAd?.backgroundColor = Color.parseColor("#CC000000")

    // "오늘 하루 보지 않기" 텍스트 색상
    popupAd?.hideForTodayTextColor = Color.WHITE

    // 닫기 버튼 텍스트 색상
    popupAd?.closeTextColor = Color.WHITE

    // CTA 버튼 텍스트 색상
    popupAd?.ctaTextColor = Color.BLUE

    popupAd?.load()
}

커스터마이징 옵션

속성타입설명기본값
backgroundColorInt?팝업 배경(딤) 색상검은색 (투명도 0.8)
hideForTodayTextColorInt?”오늘 하루 보지 않기” 텍스트 색상흰색
closeTextColorInt?닫기 버튼 텍스트 색상흰색
ctaTextColorInt?CTA 버튼 텍스트 색상시스템 기본값

커스텀 클릭 처리

광고 클릭 시 기본 브라우저 열기 대신 커스텀 처리를 하려면 useCustomClick 속성을 사용하세요.
private fun loadPopupAd() {
    popupAd = AdropPopupAd(this, "YOUR_POPUP_UNIT_ID")

    // 커스텀 클릭 처리 활성화
    popupAd?.useCustomClick = true

    popupAd?.popupAdListener = object : AdropPopupAdListener {
        override fun onAdReceived(ad: AdropPopupAd) {
            showPopupAd()
        }

        override fun onAdFailedToReceive(ad: AdropPopupAd, errorCode: AdropErrorCode) {
            Log.d("Adrop", "광고 수신 실패: $errorCode")
        }

        override fun onAdClicked(ad: AdropPopupAd) {
            // 커스텀 클릭 처리
            Log.d("Adrop", "광고 클릭됨: ${ad.destinationURL}")

            // 원하는 URL로 열기
            popupAd?.open("https://your-custom-url.com")

            // 또는 In-App 브라우저로 열기
            // openInAppBrowser(ad.destinationURL)
        }
    }

    popupAd?.load()
}

광고 속성

팝업 광고 객체에서 다음 속성들을 사용할 수 있습니다:
unitId
String
광고 유닛 ID
isLoaded
Boolean
광고가 로드되었는지 여부
creativeId
String
현재 표시 중인 광고 크리에이티브 ID
destinationURL
String
광고 클릭 시 이동할 URL
txId
String
트랜잭션 ID (광고 노출 추적용)
campaignId
String
캠페인 ID

리스너 메서드

AdropPopupAdListener (광고 이벤트)

필수 메서드

onAdReceived
(AdropPopupAd) -> Unit
광고 수신 성공 시 호출됩니다. 이 시점에서 show()를 호출하여 광고를 표시할 수 있습니다.
onAdFailedToReceive
(AdropPopupAd, AdropErrorCode) -> Unit
광고 수신 실패 시 호출됩니다. 에러 코드를 통해 실패 원인을 확인할 수 있습니다.

선택 메서드

onAdImpression
(AdropPopupAd) -> Unit
광고 노출이 기록되었을 때 호출됩니다.
onAdClicked
(AdropPopupAd) -> Unit
사용자가 광고를 클릭했을 때 호출됩니다.
onAdWillPresentFullScreen
(AdropPopupAd) -> Unit
팝업 광고가 표시되기 직전에 호출됩니다.
onAdDidPresentFullScreen
(AdropPopupAd) -> Unit
팝업 광고가 화면에 표시된 직후 호출됩니다.
onAdWillDismissFullScreen
(AdropPopupAd) -> Unit
팝업 광고가 닫히기 직전에 호출됩니다.
onAdDidDismissFullScreen
(AdropPopupAd) -> Unit
팝업 광고가 닫힌 직후 호출됩니다. 다음 광고를 미리 로드하기 좋은 시점입니다.
onAdFailedToShowFullScreen
(AdropPopupAd, AdropErrorCode) -> Unit
광고 표시 실패 시 호출됩니다. 에러 코드를 통해 실패 원인을 확인할 수 있습니다.

AdropPopupAdCloseListener (닫기 이벤트)

모든 메서드가 선택사항입니다.
onClosed
(AdropPopupAd) -> Unit
사용자가 닫기 버튼을 클릭했을 때 호출됩니다.
onDimClicked
(AdropPopupAd) -> Unit
사용자가 팝업 외부(딤 영역)를 클릭했을 때 호출됩니다.
onTodayOffClicked
(AdropPopupAd) -> Unit
사용자가 “오늘 하루 보지 않기”를 선택했을 때 호출됩니다. 이 콜백에서 날짜를 저장하여 하루 동안 광고를 표시하지 않도록 구현할 수 있습니다.

”오늘 하루 보지 않기” 구현

사용자가 “오늘 하루 보지 않기”를 선택했을 때 적절히 처리하는 방법입니다.

SharedPreferences를 이용한 구현

class PopupAdManager(private val context: Context) {
    private val prefs = context.getSharedPreferences("adrop_prefs", Context.MODE_PRIVATE)
    private val todayOffKey = "popup_ad_today_off_date"
    private var popupAd: AdropPopupAd? = null

    // 오늘 광고를 표시할 수 있는지 확인
    fun canShowAdToday(): Boolean {
        val lastHiddenDate = prefs.getLong(todayOffKey, 0L)
        if (lastHiddenDate == 0L) return true

        val calendar = Calendar.getInstance()
        calendar.timeInMillis = lastHiddenDate
        val hiddenDay = calendar.get(Calendar.DAY_OF_YEAR)
        val hiddenYear = calendar.get(Calendar.YEAR)

        calendar.timeInMillis = System.currentTimeMillis()
        val currentDay = calendar.get(Calendar.DAY_OF_YEAR)
        val currentYear = calendar.get(Calendar.YEAR)

        // 저장된 날짜가 오늘이 아니면 표시 가능
        return hiddenDay != currentDay || hiddenYear != currentYear
    }

    // 광고 로드 및 표시
    fun loadAndShowAd(activity: Activity, unitId: String) {
        if (!canShowAdToday()) {
            Log.d("Adrop", "오늘은 광고를 표시하지 않습니다")
            return
        }

        popupAd?.destroy()
        popupAd = AdropPopupAd(activity, unitId)
        popupAd?.popupAdListener = popupAdListener
        popupAd?.closeListener = popupCloseListener
        popupAd?.load()
    }

    private val popupAdListener = object : AdropPopupAdListener {
        override fun onAdReceived(ad: AdropPopupAd) {
            // 광고 수신 후 자동 표시
            if (context is Activity) {
                ad.show(context)
            }
        }

        override fun onAdFailedToReceive(ad: AdropPopupAd, errorCode: AdropErrorCode) {
            Log.d("Adrop", "광고 수신 실패: $errorCode")
        }
    }

    private val popupCloseListener = object : AdropPopupAdCloseListener {
        override fun onTodayOffClicked(ad: AdropPopupAd) {
            // 오늘 날짜를 저장
            prefs.edit().putLong(todayOffKey, System.currentTimeMillis()).apply()
            Log.d("Adrop", "오늘 하루 팝업 광고를 표시하지 않습니다")
        }
    }

    fun destroy() {
        popupAd?.destroy()
        popupAd = null
    }
}

사용 예시

class MainActivity : AppCompatActivity() {
    private lateinit var popupAdManager: PopupAdManager

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

        popupAdManager = PopupAdManager(this)

        // 앱 시작 시 팝업 광고 표시
        popupAdManager.loadAndShowAd(this, "YOUR_POPUP_UNIT_ID")
    }

    override fun onDestroy() {
        super.onDestroy()
        popupAdManager.destroy()
    }
}

팝업 타입

팝업 광고는 두 가지 타입으로 제공됩니다:
타입설명위치
POPUP_BOTTOM하단 팝업화면 하단에서 올라오는 형태
POPUP_CENTER중앙 팝업화면 중앙에 표시되는 형태
팝업 타입은 애드컨트롤 콘솔에서 광고 유닛 생성 시 설정됩니다. 코드에서 별도로 지정할 필요가 없습니다.

베스트 프랙티스

1. 적절한 표시 시점

팝업 광고는 다음 시점에 표시하는 것이 효과적입니다:
// ✅ 좋은 예: 앱 시작 시
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    loadAndShowPopupAd()
}

// ✅ 좋은 예: 콘텐츠 로드 완료 시
fun onContentLoaded() {
    if (shouldShowPopupAd()) {
        showPopupAd()
    }
}

// ✅ 좋은 예: 특정 이벤트 완료 시
fun onAchievementUnlocked() {
    showPopupAd()
}

// ❌ 나쁜 예: 사용자가 작업 중일 때
fun onUserTyping() {
    showPopupAd() // 사용자 경험 저해
}

2. “오늘 하루 보지 않기” 존중

사용자가 “오늘 하루 보지 않기”를 선택했다면 반드시 준수하세요.
fun shouldShowPopupAd(): Boolean {
    // 오늘 하루 보지 않기 확인
    if (!canShowAdToday()) {
        return false
    }

    // 다른 조건 확인
    return true
}

3. 빈도 제한

팝업 광고를 너무 자주 표시하지 마세요.
class PopupAdFrequencyManager(private val context: Context) {
    private val prefs = context.getSharedPreferences("adrop_prefs", Context.MODE_PRIVATE)
    private val lastShownKey = "popup_ad_last_shown"
    private val minimumInterval = 3600000L // 1시간 (밀리초)

    fun canShowAd(): Boolean {
        // 오늘 하루 보지 않기 확인
        if (!canShowAdToday()) {
            return false
        }

        // 최소 시간 간격 확인
        val lastShown = prefs.getLong(lastShownKey, 0L)
        if (lastShown == 0L) return true

        val currentTime = System.currentTimeMillis()
        return currentTime - lastShown >= minimumInterval
    }

    fun recordAdShown() {
        prefs.edit().putLong(lastShownKey, System.currentTimeMillis()).apply()
    }

    private fun canShowAdToday(): Boolean {
        // 위의 "오늘 하루 보지 않기" 구현 참고
        return true
    }
}

4. 광고 미리 로드

사용자 경험을 위해 광고를 미리 로드하세요.
class MainActivity : AppCompatActivity() {
    private var popupAd: AdropPopupAd? = null
    private var isAdReady = false

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

        // 미리 로드
        preloadPopupAd()
    }

    private fun preloadPopupAd() {
        popupAd = AdropPopupAd(this, "YOUR_POPUP_UNIT_ID")
        popupAd?.popupAdListener = object : AdropPopupAdListener {
            override fun onAdReceived(ad: AdropPopupAd) {
                isAdReady = true
            }

            override fun onAdFailedToReceive(ad: AdropPopupAd, errorCode: AdropErrorCode) {
                Log.d("Adrop", "광고 수신 실패: $errorCode")
            }
        }
        popupAd?.load()
    }

    fun showPopupAdIfReady() {
        if (isAdReady) {
            popupAd?.show(this)
        }
    }
}

5. 가로 모드 처리

팝업 광고는 세로 모드에서만 지원됩니다. 가로 모드에서는 onAdFailedToShowFullScreen이 호출됩니다.
private val popupAdListener = object : AdropPopupAdListener {
    override fun onAdReceived(ad: AdropPopupAd) {
        // 세로 모드일 때만 표시
        if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
            ad.show(this@MainActivity)
        }
    }

    override fun onAdFailedToShowFullScreen(ad: AdropPopupAd, errorCode: AdropErrorCode) {
        if (errorCode == AdropErrorCode.ERROR_CODE_LANDSCAPE_UNSUPPORTED) {
            Log.d("Adrop", "팝업 광고는 세로 모드에서만 지원됩니다")
        }
    }

    override fun onAdFailedToReceive(ad: AdropPopupAd, errorCode: AdropErrorCode) {
        Log.d("Adrop", "광고 수신 실패: $errorCode")
    }
}

테스트 유닛 ID

개발 및 테스트 시 다음 테스트 유닛 ID를 사용하세요.
광고 타입테스트 유닛 ID
팝업 (하단 이미지)PUBLIC_TEST_UNIT_ID_POPUP_BOTTOM
팝업 (중앙 이미지)PUBLIC_TEST_UNIT_ID_POPUP_CENTER
팝업 비디오 (하단 16:9)PUBLIC_TEST_UNIT_ID_POPUP_BOTTOM_VIDEO_16_9
팝업 비디오 (하단 9:16)PUBLIC_TEST_UNIT_ID_POPUP_BOTTOM_VIDEO_9_16
팝업 비디오 (중앙 16:9)PUBLIC_TEST_UNIT_ID_POPUP_CENTER_VIDEO_16_9
팝업 비디오 (중앙 9:16)PUBLIC_TEST_UNIT_ID_POPUP_CENTER_VIDEO_9_16

테스트 광고 사용 예시

// 테스트 환경과 프로덕션 환경 분리
val popupUnitId = if (BuildConfig.DEBUG) {
    "PUBLIC_TEST_UNIT_ID_POPUP_BOTTOM"
} else {
    "YOUR_PRODUCTION_UNIT_ID"
}

popupAd = AdropPopupAd(this, popupUnitId)
실제 배포 시에는 반드시 애드컨트롤 콘솔에서 생성한 실제 유닛 ID를 사용하세요.

완전한 예제

import android.graphics.Color
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import io.adrop.ads.model.AdropErrorCode
import io.adrop.ads.popupAd.AdropPopupAd
import io.adrop.ads.popupAd.AdropPopupAdCloseListener
import io.adrop.ads.popupAd.AdropPopupAdListener

class PopupAdActivity : AppCompatActivity() {
    private var popupAd: AdropPopupAd? = null

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

        // 버튼 클릭 시 광고 로드 및 표시
        findViewById<Button>(R.id.btn_show_popup).setOnClickListener {
            loadPopupAd()
        }
    }

    private fun loadPopupAd() {
        // 기존 광고 정리
        popupAd?.destroy()

        // 팝업 광고 생성
        popupAd = AdropPopupAd(this, "YOUR_POPUP_UNIT_ID")

        // 커스터마이징
        popupAd?.apply {
            backgroundColor = Color.parseColor("#CC000000")
            hideForTodayTextColor = Color.WHITE
            closeTextColor = Color.WHITE
            ctaTextColor = Color.BLUE
            useCustomClick = false
        }

        // 리스너 설정
        popupAd?.popupAdListener = object : AdropPopupAdListener {
            override fun onAdReceived(ad: AdropPopupAd) {
                Log.d("Adrop", "팝업 광고 수신 완료")
                showPopupAd()
            }

            override fun onAdFailedToReceive(ad: AdropPopupAd, errorCode: AdropErrorCode) {
                Log.d("Adrop", "팝업 광고 수신 실패: $errorCode")
            }

            override fun onAdImpression(ad: AdropPopupAd) {
                Log.d("Adrop", "팝업 광고 노출")
                Log.d("Adrop", "txId: ${ad.txId}, campaignId: ${ad.campaignId}")
            }

            override fun onAdClicked(ad: AdropPopupAd) {
                Log.d("Adrop", "팝업 광고 클릭: ${ad.destinationURL}")
            }

            override fun onAdDidPresentFullScreen(ad: AdropPopupAd) {
                Log.d("Adrop", "팝업 광고 표시됨")
            }

            override fun onAdDidDismissFullScreen(ad: AdropPopupAd) {
                Log.d("Adrop", "팝업 광고 닫힘")
                // 다음 광고 미리 로드
                loadPopupAd()
            }

            override fun onAdFailedToShowFullScreen(ad: AdropPopupAd, errorCode: AdropErrorCode) {
                Log.d("Adrop", "팝업 광고 표시 실패: $errorCode")
            }
        }

        popupAd?.closeListener = object : AdropPopupAdCloseListener {
            override fun onClosed(ad: AdropPopupAd) {
                Log.d("Adrop", "닫기 버튼 클릭")
            }

            override fun onDimClicked(ad: AdropPopupAd) {
                Log.d("Adrop", "딤 영역 클릭")
            }

            override fun onTodayOffClicked(ad: AdropPopupAd) {
                Log.d("Adrop", "오늘 하루 보지 않기 선택")
                saveHiddenDate()
            }
        }

        // 광고 로드
        popupAd?.load()
    }

    private fun showPopupAd() {
        popupAd?.let { ad ->
            if (ad.isLoaded) {
                ad.show(this)
            }
        }
    }

    private fun saveHiddenDate() {
        val prefs = getSharedPreferences("adrop_prefs", MODE_PRIVATE)
        prefs.edit().putLong("last_popup_hidden_date", System.currentTimeMillis()).apply()
    }

    override fun onDestroy() {
        super.onDestroy()
        popupAd?.destroy()
    }
}

문제 해결

광고가 표시되지 않음

  • SDK가 초기화되었는지 확인
  • 유닛 ID가 올바른지 확인
  • 네트워크 연결 상태 확인
  • Adrop SDK 초기화 시 production 설정 확인
  • 에러 코드를 확인하여 원인 파악
  • 테스트 환경에서는 테스트 유닛 ID 사용
  • 광고 인벤토리 부족 시 나중에 재시도
  • ERROR_CODE_AD_HIDE_FOR_TODAY: “오늘 하루 보지 않기”가 활성화된 상태
  • onAdReceived 콜백 이후에 show()를 호출했는지 확인
  • Activity가 유효한 상태인지 확인
  • isLoaded 속성을 확인하여 광고가 로드되었는지 확인
  • 세로 모드인지 확인 (가로 모드는 지원되지 않음)
팝업 광고는 세로 모드에서만 지원됩니다. 가로 모드에서 show()를 호출하면 onAdFailedToShowFullScreenERROR_CODE_LANDSCAPE_UNSUPPORTED 에러 코드와 함께 호출됩니다.

”오늘 하루 보지 않기”가 작동하지 않음

// 날짜 비교 로직 확인
fun isToday(timestamp: Long): Boolean {
    val calendar = Calendar.getInstance()
    val today = calendar.apply {
        timeInMillis = System.currentTimeMillis()
    }

    val targetDay = Calendar.getInstance().apply {
        timeInMillis = timestamp
    }

    return today.get(Calendar.YEAR) == targetDay.get(Calendar.YEAR) &&
           today.get(Calendar.DAY_OF_YEAR) == targetDay.get(Calendar.DAY_OF_YEAR)
}

// 사용
fun canShowAdToday(): Boolean {
    val prefs = getSharedPreferences("adrop_prefs", Context.MODE_PRIVATE)
    val lastHiddenDate = prefs.getLong("last_popup_hidden_date", 0L)

    return lastHiddenDate == 0L || !isToday(lastHiddenDate)
}

관련 문서