월드 숍 서비스 활용하기

# 학습 과정 소개

`WorldShopService`를 활용해 웹에서 등록한 아이템, 이용권을 구매할 수 있게 만들어봅시다. 출시하지 않은 월드에서는 상품을 출시하더라도, 실제로 판매할 수 없음을 유념하세요.

# WorldShopService

등록한 상품을 월드에서 활용할 때는 `WorldShopService`를 활용해야 합니다. 구매 성공한 유저는 크리에이터가 제공한 상품을 지급 받아야 합니다.

# 활용 예시

아이템과 이용권을 등록합니다. 아이템을 구매하면 즉시 포션 수를 100으로 증가시키고, 이용권을 구매한 뒤 공격을 누르면 포션 수를 15개 증가시킵니다.

[gif](16690340160770272c9d6580a42c99c9be10128f2e9f7)

#### 사전 준비

[상품 등록하기](/docs?postId=581)를 따라 아이템과 이용권을 등록합니다.

[{"상품 종류":"상품 이름","아이템":"포션 아이템","이용권":"포션 추가 PASS"},{"상품 종류":"가격","아이템":"무료","이용권":"무료"},{"상품 종류":"판매 기간","아이템":"무기한","이용권":"무기한"},{"상품 종류":"상품 섬네일","아이템":"[png](1669032688633b964bbc6fc0b4f5784c7bf5e7461f58f)","이용권":"[png](1669032702800d3d79eebd2dd463990f7b0fe404b06e7)"},{"상품 종류":"상품 설명","아이템":"무료 포션 100개를 얻을 수 있습니다.","이용권":"포션을 추가로 얻을 수 있습니다."}]

#### 상품 구매창 UI 만들기

  1. UI 에디터 - DefaultGroup - Create Entity - Create UIEmpty를 선택해 배치하고, 이름을 MiniShop으로 변경합니다.
  2. 새로운 Item 엔티티를 생성해 MiniShop 엔티티 하위에 배치한 뒤 텍스트를 활용해 아래 그림과 같이 이미지와 상품 구매창을 만듭니다. [png](16686688390882a43ba5bfd2842c7821fc9e59cc23a25)
  3. World Shop Viewer에서 상품을 선택하고, 콘텍스트 메뉴를 열어 Copy Item Thumbnail RUID를 선택합니다. [png](166877214605218b6051c1c4145f9b9b55ee2c391130d)
  4. 상품 이미지를 보여줄 이미지 엔티티를 선택하고, SpriteGUIRendererComponent의 ImageRUID에 상품 RUID를 붙여 넣습니다.
  5. 만든 Item 엔티티를 복사해 MiniShop 엔티티 하위에 나란히 배치합니다. [png](16687721843201c7dbc6d49c3468eb14a56c815b8b21f)
  6. 복사한 엔티티의 이름을 Pass로 변경하고, 이용권 RUID를 붙여넣습니다.
  7. MiniShop의 Enable 프로퍼티를 [png](CheckboxOff)해제합니다.



#### 상점 버튼 UI 만들기

상품 구매창을 열고 닫는 버튼을 만들어봅시다. [png](1669181482837083a5716f94f4b9dac17c23994305949)

  1. UI 에디터 - DefaultGroup - ShopOpenBtn 버튼을 배치합니다.
  2. SpriteGUIRendererComponent - ImageRUID에 상점 모양 스프라이트 찾아 적용합니다. 
     * ImageRUID: 92a05d93672f65b4f991279cf1f0b4e6
  3. 새로운 OpenShopBtn 스크립트 컴포넌트를 생성하고, ShopOpenBtn에 추가합니다. [png](1668771870564d9e4aa0cc7d84cf78a4d010cf2254125)
  4. ShopOpenBtn을 누르면 Minishop을 열고 닫을 수 있게 아래와 같이 작성합니다.


    
    
    @Component
    script ShopUIToggler extends Component
        @ExecSpace("ClientOnly")
        method void OpenShop()
        local Shop = _EntityService:GetEntityByPath("/ui/DefaultGroup/MiniShop")
        local Item = Shop:GetChildByName("Item") 
        local Pass = Shop:GetChildByName("Pass") 
    
        if Shop.Enable == false then
    
            Shop:SetEnable(true)   
    
        else
    
            Shop:SetEnable(false)
    
        end
        end
    
        @EventSender("Self")
        handler HandleButtonClickEvent(ButtonClickEvent event)
        -- Parameters
        local Entity = event.Entity
        --------------------------------------------------------
        self:OpenShop()
        end
    end
    

#### 상품 획득 확인 UI 만들기

상품 구매에 성공했을 때 포션 개수와 이용권 보유 여부를 확인할 수 있는 ui를 만들어봅시다.

  1. UI 에디터 - DefaultGroup - MyPotionInfo 이미지 엔티티를 배치합니다.
  2. 하위에 이미지와 텍스트 엔티티를 배치하고, 아래의 그림처럼 이름을 변경합니다. [png](166918924041307df0d08c4824221a462c16df2e2b840)



#### 상품 정보를 구매창에 불러오기

  1. 새로운 ItemSlot 스크립트 컴포넌트를 생성하고 Item, Pass UI엔티티에 추가합니다.

  2. ItemSlot 컴포넌트 스크립트 에디터 창을 열고, 프로퍼티를 추가합니다.
         
         @Component
         script ItemPurchaseHandler extends Component
         property string ItemID = ""
         property Entity PurchaseBtn = nil
         end
         



    
    
    3. World Shop Viewer 에서 상품의 콘텍스틀 메뉴를 열어 Copy ID를 선택합니다.
    [png](16691907394815f8bdb41e2224869a012f97b5d142317)
    4. 복사한 ID를 각각의 Item, Pass의 ItemSlot 컴포넌트의 ID에 추가합니다. 
    [png](1669191109035f825047ab3a2458ab3e37daa4f3c6401)
    5. 상품 구매창이 열릴 때마다 정보를 갱신할 수 있도록, **MyDesk - Create Scripts - Create EventType**을 선택해 새로운 **ShopOpenEvent**를 만듭니다.
    [png](166903451819499a0605245bf40c5ad0497ac93cacefb)
    6. **ItemSlot** 컴포넌트에 `WorldShopService:PromptPurchase()`를 활용해 구매 버튼을 누르면 상품 구매창에 상품 정보가 보이도록 아래와 같이 작성합니다.
    
        ```lua
    @Component
    script ItemPurchaser extends Component
        @ExecSpace("ClientOnly")
        method void PurchaseItem()
        _WorldShopService:PromptPurchase(self.ItemID)
        end
    
        @ExecSpace("ClientOnly")
        method void OnBeginPlay()
                self.PurchaseBtn = self.Entity:GetChildByName("PurchaseBtn")
        --         self.PurchaseBtn:ConnectEvent("ButtonClickEvent", function(event) 
        --             self:PurchaseItem()
        --         end)
        -- [Commented: MLUA requires @EventSender + handler pattern]
        end
    end
    

  7. 상점 버튼을 누를 때의 이벤트를 받도록 Event Handler에 ShopOpenEvent를 추가합니다. self를 눌러 Entity로 변경하고, 버튼 엔티티를 지정합니다. [png](1668773199934df13c978c30e49b0989d36024f8a5ce4)
         
         @Component
         script ShopItemRefresher extends Component
         @ExecSpace("ClientOnly")
         method void RefreshShop()
         self._T.ItemName = nil
         self._T.ItemName =  self.Entity:GetChildByName("ItemName").TextComponent
         local SearchResult = _WorldShopService:GetProductAndWait(self.ItemID)
         self._T.ItemName.Text = SearchResult.Name
         end
         
         @EventSender("Self")
         handler HandleShopOpenEvent(any event)
         -- Parameters
         --------------------------------------------------------
         self:RefreshShop()
         end
         end
         



    
    
    #### 상품 지급하기 
     **MyShopLogic**에 포션 상품 구매에 성공하는 경우 포션 100개를 얻을 수 있도록 아래와 같이 입력합니다.
     `WorldShopService:SetProcessPurchaseCallback`을 활용해 상품 구매가 발생했을 때 상품을 지급합니다. 상품을 지급이 성공하면 true를, 실패하면 false를 반환해야 합니다.
    
    

@Component script PurchaseRewardHandler extends Component @ExecSpace("ServerOnly") method void OnBeginPlay() _WorldShopService:SetProcessPurchaseCallback(self.ProcessPurchase) end
    
    
    @ExecSpace("ServerOnly")
    method boolean ProcessPurchase(any purchaseInfo)
    local userEntity = _UserService:GetUserEntityByUserId(purchaseInfo.UserId)
    
    if _EntityService:IsValid(userEntity) == false then
        log_error("유효하지 않은 사용자! PurchaseId: " .. purchaseInfo.PurchaseId .. " / " ..
                    "UserId: " .. purchaseInfo.UserId .. " / " ..
                    "ProductId: " .. purchaseInfo.ProductId)
        return false   
    end
    
    if purchaseInfo.ProductId == "ItemID를 입력하세요" then     
        userEntity.PlayerReward:GetPotion(100)
    end
    if purchaseInfo.ProductId == "PassID를 입력하세요" then
    
        userEntity.PlayerReward:PassCheck()
    
    end
    
    return true
    end
    

end
    
    
    #### UI에 상품 구매 여부 반영하기
    상품이 구매한 유저에게 반영될 수 있도록 만들어 봅시다.
    1. 새로운 **PlayerReward** 컴포넌트를 생성하고, **DefaultPlayer**에 추가합니다.
    2. 아래와 같이 프로퍼티를 추가합니다.
    
        ```lua
    @Component
    script PotionPassState extends Component
        property any PotionValue = "/ui/DefaultGroup/MyPotionInfo/PotionValue"
        @Sync
        property number Potion = 0
        property any PotionValue_2 = "/ui/DefaultGroup/MyPotionInfo/PassValue"
        @Sync
        property boolean HasPass = false
    end
    

  3. 포션 아이템을 구매했을 때 100개를 지급 받을 수 있도록 아래와 같이 작성합니다. 
         
         @Component
         script PlayerPotionDisplay extends Component
         method void GetPoint(number value)
         self.Potion = self.Potion + Value
         end
         
         @ExecSpace("ClientOnly")
         method void OnSyncProperty(string name, any value)
         if self.Entity ~= _UserService.LocalPlayer then
             return
         end
         
         if name == "Potion" then
             self.PotionValue.Text = value
         end
         end
         end
         



    
    
    4. 포션 Pass 이용권을 구매했을 때 이용권 보유 UI가 변경되도록 아래와 같이 작성합니다. 이용권 ID에는 등록한 이용권 ID를 입력합니다.
    
        ```lua
    @Component
    script PlayerPassVerifier extends Component
        @ExecSpace("ClientOnly")
        method void PassCheck()
        if self.Entity ~= _UserService.LocalPlayer then 
            return 
        end
    
        if _WorldShopService:UserHasPassAndWait(self.Entity.Name, "이용권 ID") then 
            self.PassValue.Text = "O"
            self:PassUpdate(true)
        else
            self.PassValue.Text = "x"
            self:PassUpdate(false)
        end
        end
    
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
        self:PassCheck()
        end
    
        @ExecSpace("ServerOnly")
        method void PassUpdate(boolean value)
        self.HasPass = value
        end
    end
    

  5. 이용권이 있을 때 공격을 누를 때마다 추가로 포션 15개를 획득할 수 있게 아래와 같이 작성합니다.
         
         @Component
         script AttackPotionGrantor extends Component
         @ExecSpace("ServerOnly")
         method void OnBeginPlay()
                     -- Parameters
                     local ActionName = event.ActionName
                     local PlayerEntity = event.PlayerEntity
                     --------------------------------------------------------
                     if self:IsServer() == false then 
                         return 
                     end 
         
                     if self.HasPass == false then 
                         return 
                     end 
         
                     if ActionName ~= "Attack" then 
                         return 
                     end 
         
                     self:GetPotion(15)
         end
         end
         



    
    
    > **Tip**
    > 이용권은 한 번만 구매할 수 있으므로 재구매가 가능한지 사전 확인이 필요합니다.
    > 이미 구매한 이용권을 구매 버튼을 누르면 구매 불가 안내창이 나타납니다.
    > [png](1669032829433abbe55794afa4b9c8aa997f6831de580)
    
    > **Tip**
    > 구매한 유저만 소유할 수 있는지 이용권 출시 전 반드시 확인해야 합니다.
    > 메이커에서 테스트용 유저를 추가해 이용권을 구매한 사람만  소유할 수 있는지 확인 후 월드 출시하기를 권장합니다.
    
    
    ##### 참고 가이드
    ‘유료’ 확률형 콘텐츠를 제공하는 경우 확률 정보 공개 가이드에 따라 콘텐츠 사용 전 사용자에게 고지해야 할 의무가 있습니다. [확률 정보 공개](/docs/?postId=667)를 참고해 상품을 제작하길 바랍니다.
    * [상품 등록하기](/docs?postId=581)
    * [상품 관리하기](/docs?postId=658)