> For the complete documentation index, see [llms.txt](https://docs.overdare.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.overdare.com/korean/manual/monetization/marketplace.md).

# 월드 상품 판매

## 개요

크리에이터는 Marketplace 기능을 활용하여 골드, 크리스탈과 같은 **게임 내 재화**나 총, 탄약, 회복 물약 등의 **아이템**을 월드 상품으로 구성하여 사용자에게 **판매**할 수 있습니다.

## 판매자의 의무

크리에이터는 판매한 상품이 구매자에게 **정상적으로 지급되도록 처리할 의무**가 있습니다. 지급 누락이나 설명과 다른 상품 구성, 구매한 상품의 가치 훼손 등 **문제가 발생한 경우**, 반드시 이에 대한 적절한 해결 조치를 **성실히 이행**해야 합니다. 지급 의무를 불성실하게 이행하거나 관련 신고가 반복적으로 접수될 경우, 정산 보류, 월드 제재, 나아가 계정 비활성화 등의 **불이익이 발생**할 수 있습니다.

자세히 알아보기

{% content-ref url="/pages/p0hyzbFWf4iszJBVf21a" %}
[월드 상품 가이드라인](/korean/overdare/policy/world-product-guidelines.md)
{% endcontent-ref %}

### 문의 창구 설정

상품 판매가 포함된 월드는 **반드시 소셜 링크를 설정**해야 하며, 이는 크리에이터가 상품 관련 문의에 대해 플레이어와 직접 소통할 수 있도록 하기 위함입니다.

**소셜 링크가 설정되지 않은 월드는 상품 등록을 할 수 없습니다.**

<figure><img src="/files/39u4Hxw8Q8ad7Tzl8N2b" alt=""><figcaption></figcaption></figure>

등록한 소셜 링크는 OVERDARE App에서 월드를 클릭하면 노출되는 팝업에서 **Detail 버튼**을 누르면 노출됩니다.

<figure><img src="/files/19lDMYlWFPRLnU92xvn9" alt=""><figcaption></figcaption></figure>

## 상품 종류

<table><thead><tr><th width="152.33331298828125">상품 종류</th><th>용도</th><th>예시</th></tr></thead><tbody><tr><td>World Product</td><td>1회성으로 지급되는 재화, 아이템 등의 상품</td><td><p>재화 지급(골드, 크리스탈 등)</p><p>아이템 지급(검, 도끼, 회복 포션 등)</p><p>소모성 아이템(부활, 건물 즉시 설치권 등)</p></td></tr></tbody></table>

## 상품 관리

상품은 월드별로 구성되며, 각각 개별적으로 설정할 수 있습니다.

상품을 등록하고 관리하려면 **월드를 먼저 Publish**해야 합니다. 월드를 Publish 할 때 Owner group을 그룹으로 설정하면 그룹원과 함께 상품을 관리하고 수익을 분배할 수 있습니다.

### 권한별 기능

<table><thead><tr><th width="274">권한</th><th width="150.333251953125">상품 조회/등록</th><th width="162.0001220703125">상품 수정</th><th>수익 분배</th></tr></thead><tbody><tr><td>월드제작자(개인 월드)</td><td>O</td><td>O</td><td>O</td></tr><tr><td>Group Owner(그룹 월드)</td><td>O</td><td>O</td><td>O</td></tr><tr><td>Group Member(그룹 월드)</td><td>O</td><td>X</td><td>X</td></tr></tbody></table>

### 상품 목록 확인

Dashboard의 World 탭에서 월드를 클릭하여 월드 페이지에 진입한 다음, **World Product 탭**을 클릭하면 등록한 상품을 확인할 수 있습니다.

<figure><img src="/files/1tUAJoV5dUfFQ4dMMnp4" alt=""><figcaption></figcaption></figure>

### 상품 등록

**+ Create World Product 버튼**을 눌러 새로운 상품을 등록할 수 있습니다.

<figure><img src="/files/k8bffkIYrNVQr55Bpk4G" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/B3H1oNDxJGcJ8N1X9qf5" alt=""><figcaption></figcaption></figure>

### 상품 ID 복사하기

각 상품 이름 옆에 표시되는 **... 버튼**을 누른 다음, **Copy Product Id**를 클릭해서 상품 ID를 복사할 수 있습니다.

<figure><img src="/files/2v2I9dQnHWuPD57pEnfV" alt=""><figcaption></figcaption></figure>

### 상품 수정

상품 이미지나 이름 영역을 클릭하여 상품의 이미지, 이름, 가격 등을 수정할 수 있습니다.

<figure><img src="/files/jsf4FqKV8OqMq9zbTzRN" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
유저에게 **이미 구매창이 출력된 상태**에서 상품 수정이 발생하면, 결제시에 **이전 정보(가격)**&#xC73C;로 처리됩니다.
{% endhint %}

## 상품 판매를 위한 스크립트 처리

Marketplace를 통한 상품 구매 기능은 **유저의 재화를 소모하는 중요 기능**이므로, pcall 등을 활용한 **엄격한 예외 처리와 디버깅이 필수적**입니다. 이를 통해 런타임 오류나 네트워크 지연 등의 상황에서도 **시스템의 안정성을 유지**할 수 있으며, 아이템 미지급과 같은 **치명적인 문제를 사전에 방지**할 수 있습니다.

### 기능 목록

<table><thead><tr><th width="348.3333740234375">기능</th><th>설명</th></tr></thead><tbody><tr><td>GetProductInfo(productId, productType)</td><td>특정 상품 정보 요청</td></tr><tr><td>GetWorldProductsAsync()</td><td>모든 월드 상품 정보 요청</td></tr><tr><td>PromptProductPurchase(player, productId)</td><td>구매 요청</td></tr><tr><td>PromptProductPurchaseFinished</td><td>구매창이 꺼질 때 필요한 이벤트 처리</td></tr><tr><td>ProcessReceipt</td><td>구매 성공시 상품 지급 처리 후, 정상 지급건의 <strong>영수증 상태를 완료로 변경</strong>하기 위한 이벤트</td></tr></tbody></table>

### 특정 상품 정보 요청(GetProductInfo)

상품 ID(productId)와 상품 타입(Enum.InfoType)에 해당하는 상품 정보를 반환합니다.

```lua
local MarketplaceService = game:GetService("MarketplaceService")

local function Request_GetProductInfo(productId)
    local success, errorOrProductInfo = pcall(function()
        return MarketplaceService:GetProductInfo(productId, Enum.InfoType.Product)
    end)
    
    if not success then
        print("Error: " .. errorOrProductInfo .. " / ProductId : " .. productId)
        
    else
        local productInfo = errorOrProductInfo 
        print("World Product Name: " .. tostring(productInfo.Name))
        print("ProductId: " .. tostring(productInfo.ProductId))
        print("ProductType: " .. tostring(productInfo.ProductType))
        print("PriceInBLUC: " .. tostring(productInfo.PriceInBLUC))
        print("Description: " .. tostring(productInfo.Description))
        print("Created: " .. productInfo.Created)
        print("Updated: " .. productInfo.Updated)
        
        -- UI에 출력
    end
end
```

### 모든 월드 상품 정보 요청(GetWorldProductsAsync)

모든 월드 상품에 대한 정보를 포함하는 Pages 객체를 반환합니다.

```lua
local MarketplaceService = game:GetService("MarketplaceService")

local function Request_GetWorldProductsAsync()
    local success, errorOrWorldProducts = pcall(function()
        return MarketplaceService:GetWorldProductsAsync()
    end) 
    
    if not success then
        print("Error: " .. errorOrWorldProducts)
        
    else
        local worldProducts = errorOrWorldProducts
        
        local pageCount = 1	  
        local dataList = {}
			
        while true do
            local currentPage = worldProducts:GetCurrentPage()	
		    
            -- 마지막 페이지이면 루프 탈출 
            if worldProducts.IsFinished or currentPage == nil then           	
                print(pageCount .. " page IsFinished : " .. tostring(worldProducts.IsFinished))
                break
            else
                worldProducts:AdvanceToNextPageAsync()
                pageCount = pageCount + 1
            end
	    
            -- 한 페이지에 최대 100개의 상품 정보 구성
            for _, productInfo in pairs(currentPage) do		
                local i = #dataList + 1
				
                print("------ " .. i .. " ------")
                print("World Product Name: " .. tostring(productInfo.Name))
                print("ProductId: " .. tostring(productInfo.ProductId))
                
                print("ProductType: " .. tostring(productInfo.ProductType))
                print("PriceInBLUC: " .. tostring(productInfo.PriceInBLUC))
                print("Description: " .. tostring(productInfo.Description))
                print("Created: " .. productInfo.Created)
                print("Updated: " .. productInfo.Updated)
	
                table.insert(dataList, productInfo)
		
                -- UI에 출력
            end
        end
    end
end
```

### 구매 요청(PromptProductPurchase)

월드 상품 ID(productId)에 해당하는 상품의 구매를 요청합니다.\
(시스템 UI로 구매창이 출력됩니다.)

```lua
local MarketplaceService = game:GetService("MarketplaceService")

local function Request_PromptProductPurchase(player, productId)
    local success, error = pcall(function()
        MarketplaceService:PromptProductPurchase(player, productId)
    end)
	
    if not success then
        print("Error: " .. error .. " / ProductId : " .. productId)        
    end	
end
```

### 구매창 꺼질 때(PromptProductPurchaseFinished)

구매 요청(PromptProductPurchase)을 통해 출력된 구매창이 꺼질 때 이벤트가 호출되며, 구매를 성공하면 isPurchased에 true가 전달되고, 구매를 취소하거나 실패하면 false가 전달됩니다.

이 이벤트는 구매 창을 닫았는지 감지하기 위한 용도로만 사용해야 하며, <mark style="color:red;">**구매한 상품에 대한 지급 처리 용도로는 절대 사용하지 않아야 합니다.**</mark>

```lua
local Players = game:GetService("Players")
local MarketplaceService = game:GetService("MarketplaceService")

local function OnPromptPurchaseFinished(userId, productId, isPurchased)
    local player = Players:GetPlayerByUserId(userId)
    
    print(player.Name .. " / ProductID : " .. productId .. " / isPurchased : " .. tostring(isPurchased))
end
MarketplaceService.PromptProductPurchaseFinished:Connect(OnPromptPurchaseFinished)
```

### 구매 성공시 지급 처리(ProcessReceipt)

구매 성공한 상품 중에서 아직 **지급 처리가 되지 않은 영수증** 정보를 반환하는 이벤트가 호출됩니다.

#### 영수증 상태

<table><thead><tr><th width="175.6666259765625">상태</th><th width="193.3333740234375">설명</th><th>재호출 여부</th></tr></thead><tbody><tr><td>NotProcessedYet</td><td>상품 <strong>미지급</strong> 상태</td><td>호출 조건에 따라 <strong>다시 호출될 수 있음</strong></td></tr><tr><td>PurchaseGranted</td><td>상품 <strong>지급</strong> 완료 상태</td><td>다시 호출되지 않음</td></tr></tbody></table>

#### 호출 조건

* 월드 상품을 성공적으로 구매했을 때(구매 성공 팝업이 사용자에게 표시되었을 때)
  * 미처리 상품이 있는 상태에서 새로운 상품 구매시, **이전 미처리건도 함께 호출**됩니다.
* 사용자가 서버에 **접속(재접속)**&#xD588;을 때

#### 지급 상태 변경 방법

* 상품 지급 처리 후에, Enum.ProductPurchaseDecision.**PurchaseGranted**를 반환합니다.

#### 주의사항

* ProcessReceipt 이벤트 연결은 **서버측 Script에서 한 번만** 설정해야 합니다.
* 이 콜백은 시간 제한 없이 **yield 가능**하며, 서버가 실행 중인 한 응답이 돌아올 때까지 유효합니다.
* 미지급된 영수증이 여러개인 경우 **각각 호출**되며, 콜백 호출 순서는 비결정적(non-deterministic)입니다.
* 사용자가 **서버에 있어야** 콜백이 호출됩니다.
  * 단, 콜백의 결과는 사용자가 서버에 없어도 백엔드에 기록될 수 있습니다.
* 콜백에서 **PurchaseGranted**를 반환해도 백엔드 기록이 실패할 수 있으며, 이런 경우 영수증의 상태는 변경되지 않습니다. (미지급 상태 유지)
* **미지급 상태**의 상품은 자금이 **지급 보류 상태(Escrow)**&#xB85C; 보관됩니다.

```lua
local Players = game:GetService("Players")
local MarketplaceService = game:GetService("MarketplaceService")

local ProductDeliverer = {}

-----------------------------------------------------------------------------
-- 아래와 같이 테이블에 상품 번호 별로 함수를 구성하여 상품마다 지급 로직을 구현할 수 있습니다.
ProductDeliverer[여기에 상품 번호를 넣으세요.] = function(player)
    local success, resultOrError = pcall(function()
        -- player에게 상품 지급 및 DataStore를 이용한 저장 처리 
        
        -- Tip.
        -- DataStore로 상품 정보를 저장할 때는 
        -- 네트워크 충돌이나 경쟁 상태(race condition)를 방지하기 위해
        -- IncrementAsync 또는 UpdateAsync로 처리하는 것을 권장합니다.
                
        -- 지급 및 저장 처리가 성공적으로 완료된 경우 true 반환
        return true
    end)
    
    if success and resultOrError then
        return true
        
    else
        return false, resultOrError
    end
end

-----------------------------------------------------------------------------
-- 상품 구매 성공시 호출되는 영수증 처리용 콜백
local function OnProcessReceipt(receiptInfo)	
    -- 영수증 정보
    local success, error = pcall(function()	
        print("PurchaseId: " .. receiptInfo.PurchaseId)
        print("UserId: " .. receiptInfo.PlayerId)
        print("ProductId: " .. receiptInfo.ProductId)
        print("CurrencySpent: " .. receiptInfo.CurrencySpent)
        print("PurchaseDateTime: " .. receiptInfo.PurchaseDateTime)
    end)
    
    if not success then
        print("Error: " .. tostring(error))
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end
    
    -- 플레이어가 유효하면
    local productId = receiptInfo.ProductId        
    local userId = receiptInfo.PlayerId
    
    local player = Players:GetPlayerByUserId(userId)  
    if player == nil then
        print("Error: player is nil")
        return Enum.ProductPurchaseDecision.NotProcessedYet	
    end  
    
    -- 상품 지급 함수 호출
    local delivererFunc = ProductDeliverer[productId]
    local success, error = delivererFunc(player)
    
    -- 상품 지급 성공시
    if success then
        -- 지급 완료 상태 반환
        print("Item delivery successful / ProductId : " .. productId)
        return Enum.ProductPurchaseDecision.PurchaseGranted
        
    -- 상품 지급 실패시
    else
        print("Error: " .. tostring(error))
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end
end
MarketplaceService.ProcessReceipt = OnProcessReceipt
```

## 테스트 구매

스튜디오의 테스트 플레이 환경에서도 상품 구매 및 지급 처리를 확인할 수 있습니다.\
(재화가 차감되지 않습니다.)

## 수익 통계

수익 통계 정보는 **UTC 기준 00:00 시간**에 매일 갱신되며, Dashboard의 Analytics 탭에서 **총 수익(Total Revenue)**&#xACFC; **월드별 수익(Revenue열)**&#xC744; 확인할 수 있습니다.

<figure><img src="/files/VI2IiEAffU3GhWPPRdAq" alt=""><figcaption></figcaption></figure>

월드 이름 영역을 클릭하여 **Analytics Overview 페이지**에 진입한 다음, 본문 하단의 **Revenue Summmary 영역**에서 상품별 판매 수익을 확인할 수 있습니다.

<figure><img src="/files/PzS2GAVfJLxL8oNlx6wp" alt=""><figcaption></figcaption></figure>

### 지급 상태 등 영수증 정보 확인하기

**Export All Transactions 영역**의 표시한 버튼을 눌러 판매한 상품의 지급 상태, 판매 날짜, 구매자 이름 및 Account ID 등 **영수증 정보**를 csv 파일로 확인할 수 있습니다.

<figure><img src="/files/GJtVSTXo9ZBMkZyCxM07" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Xbe8Rr4QFPPmBpSgKhPf" alt=""><figcaption></figcaption></figure>

#### DeliveryStatus

csv 파일 내 지급 상태(DeliveryStatus)의 각 항목에 대한 설명은 아래 표를 참고하세요.

<table><thead><tr><th width="189">상태</th><th width="366">설명</th><th>점검 항목</th></tr></thead><tbody><tr><td>DELIVERED</td><td>스크립트에서 지급이 정상적으로 처리되었으며, 영수증 서버에서도 상태가 지급으로 변경됨</td><td></td></tr><tr><td>DELIVERY_FAILED</td><td>스크립트에서 지급 처리 중 실패를 반환했거나, 영수증 서버에서 상태 변경 처리에 실패함</td><td>스크립트 점검<br>구매자에게<br>월드 재입장 안내</td></tr><tr><td>CALLBACK_MISSING</td><td>스크립트에서 ProcessReceipt가 정의되지 않음</td><td>스크립트 점검</td></tr></tbody></table>

## 수익

### 수익 분배

{% content-ref url="/pages/vIuz03CGYQeBfMuE0Utt" %}
[그룹 수익 분배 가이드](/korean/manual/monetization/group-revenue-distribution-guideline.md)
{% endcontent-ref %}

### 수익 정산

{% content-ref url="/pages/5nW1Jg0kAdz5SSBKMvee" %}
[개인 정산 가이드](/korean/manual/monetization/payout-guideline.md)
{% endcontent-ref %}

## 활용 예시

* **인게임 재화**를 무료/유료로 구분한 뒤, 유료 재화의 가치를 높이고 월드 상품으로 판매하여 사용자 구매를 유도하고 수익을 촉진할 수 있습니다.
* 일반 스킨보다 **더 높은** **퀄리티의 스킨**을 월드 상품으로 구성하면, 플레이어의 개성과 꾸미기 욕구를 자극하여 구매로 이어질 수 있습니다.
* **무기, 포션, 재화** 등을 묶은 **패키지 상품**을 구성하면, 개별 아이템 대비 높은 가성비를 통해 구매 전환율을 높일 수 있습니다.
* 전투 중 사망했을 때 즉시 부활할 수 있는 **소모성 아이템**을 월드 상품으로 구성하면, 게임 흐름을 유지하면서 자연스러운 구매를 유도할 수 있습니다.
* **시즌 패스**와 같은 지속형 상품을 통해 장기적인 사용자 리텐션과 수익을 동시에 확보할 수 있습니다.
* 유료 상품 위주의 과도한 구성보다는, 일정 수준의 **무료 상품을 적절히 제공**하여 사용자 만족도를 높이고, 신뢰를 바탕으로 한 자발적인 구매를 유도하는 것이 더욱 효과적입니다.
