> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-Showcase

상품 판매 기능



게임 내에서 상품을 판매할 때 사용합니다.

GS2-Exchange와의 차이점은 진열대가 존재한다는 점입니다.
진열대에는 DisplayItem을 진열할 수 있으며, DisplayItem을 구매하기 위해 필요한 대가와 상품을 구매했을 때 얻을 수 있는 보상을 설정할 수 있습니다.

진열대에는 2종류가 있으며, 고정된 DisplayItem이 진열되는 《스탠다드 진열대》와, 일정 간격으로 진열 내용이 랜덤으로 추첨되는 《랜덤 진열대》가 있습니다.

```mermaid
graph TD
  Master["마스터 데이터"] --> Showcase["스탠다드 진열대<br/>(ShowcaseModel)"]
  Master --> RandomShowcase["랜덤 진열대<br/>(RandomShowcaseModel)"]
  Showcase --> DisplayItem["DisplayItem<br/>(SalesItem / SalesItemGroup)"]
  RandomShowcase --> RandomDisplayItem["RandomDisplayItem<br/>(stock / weight)"]
  Player["플레이어"] -- Buy --> DisplayItem
  Player -- RandomShowcaseBuy --> RandomDisplayItem
  DisplayItem -- "consumeActions / acquireActions" --> Transaction["GS2-Distributor<br/>트랜잭션 실행"]
  RandomDisplayItem -- "consumeActions / acquireActions" --> Transaction
```

## 스탠다드 진열대

스탠다드 진열대는 지정한 DisplayItem이 모두 진열됩니다.
DisplayItem에는 2종류가 있으며 《SalesItem》과 《SalesItemGroup》이 있습니다.

### SalesItem

SalesItem에는 구매하기 위해 필요한 대가와, 구매했을 때 얻을 수 있는 보상을 설정할 수 있습니다.

SalesItem은 다음 3종류의 액션으로 동작을 표현합니다.

| 항목 | 설명 |
| --- | --- |
| `verifyActions` | 구매 조건 검증 액션. 지정한 마이크로서비스의 상태를 검증하여 조건을 충족하지 않으면 구매를 거부합니다. |
| `consumeActions` | 구매 시 소비하는 액션. GS2-Money2의 통화나 GS2-Inventory의 아이템 등을 대가로 소비합니다. |
| `acquireActions` | 구매 시 입수하는 액션. GS2-Inventory·GS2-Experience·GS2-Lottery 등, 임의의 마이크로서비스에 보상으로 배포합니다. |

### SalesItemGroup

SalesItemGroup은 구매 횟수에 따라 판매되는 SalesItem이 변화하는 구조를 구현합니다.

SalesItemGroup에는 여러 SalesItem을 포함시킬 수 있으며, 목록의 마지막 상품 이외에는 GS2-Limit의 카운터 상승을 대가로 설정해야 합니다.
SalesItemGroup을 진열대에 진열할 때는 내부의 SalesItem이 구매 가능한지를 판정하여, 가장 먼저 구매 가능하다고 판정된 상품이 진열됩니다.

이 기능을 이용하면, 첫 구매에 한해 반값으로 상품을 판매하거나, 구매할 때마다 가격이 올라가는 상품을 구현하거나, 10번째 구매에는 덤을 붙이는 등의 상품을 구현할 수 있습니다.

```mermaid
graph LR
  Buy["구매 요청"] --> Check1{"SalesItem 1<br/>(첫 구매 한정)"}
  Check1 -- 구매 가능 --> Sell1["SalesItem 1 판매"]
  Check1 -- 구매 완료 --> Check2{"SalesItem 2<br/>(2~9번째)"}
  Check2 -- 구매 가능 --> Sell2["SalesItem 2 판매"]
  Check2 -- 상한 도달 --> Check3{"SalesItem 3<br/>(10번째 특전)"}
  Check3 -- 구매 가능 --> Sell3["SalesItem 3 판매"]
```

## 랜덤 진열대

랜덤 진열대는 마스터 데이터에 지정한 DisplayItem 중 지정한 개수가 랜덤으로 추첨되어 진열됩니다.

랜덤 진열대는 다음 파라미터로 동작을 제어합니다.

| 항목 | 설명 |
| --- | --- |
| `maximumNumberOfChoice` | 한 번의 추첨으로 진열되는 DisplayItem의 최대 개수 |
| `displayItems` | 추첨 대상 RandomDisplayItem 목록. 각 아이템은 `weight`(추첨 가중치)와 `stock`(재고 수)을 가집니다 |
| `baseTimestamp` / `resetIntervalHours` | 진열 내용의 재추첨 간격. 지정 시각을 기점으로 일정 시간마다 진열 내용이 갱신됩니다 |
| `salesPeriodEventId` | GS2-Schedule의 이벤트ID. 판매 가능 기간을 제한하고 싶은 경우에 지정합니다 |

추첨 결과는 플레이어별로 `RandomShowcaseStatus`로 저장되며, 구매 시 재고가 소비됩니다. 재추첨 간격이 도래하면 다음 접속 시 진열 내용이 갱신됩니다.

## 버프에 의한 보정

GS2-Buff와 연동하면 DisplayItem 및 RandomDisplayItemModel의 `acquireActions`·`verifyActions`·`consumeActions`를 동적으로 보정할 수 있으며, 랜덤 진열 상품에서는 `stock`의 덮어쓰기도 가능합니다. 이벤트나 캠페인에 맞춰 보상이나 필요 대가, 재고 수를 유연하게 변경할 수 있습니다.

## 스크립트 트리거

네임스페이스에 `buyScript`를 설정하면, 상품 구매 시점에 커스텀 스크립트를 호출할 수 있습니다.

설정할 수 있는 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.

- `buyScript`: 상품 구매 시

이를 활용하면, 구매 처리에 독자적인 검증이나 감사 로그 출력, KPI 집계 등을 끼워 넣을 수 있습니다.

## 트랜잭션 액션

GS2-Showcase에서는 다음과 같은 트랜잭션 액션을 제공하고 있습니다.

- 소비 액션: 구매 횟수 증가
- 입수 액션: 구매 횟수 감소, 랜덤 진열대의 재추첨

"랜덤 진열대의 재추첨"을 입수 액션으로 이용하면, 특정 아이템을 입수했을 때나 퀘스트 클리어 시의 보상으로서 상점의 진열 내용을 강제로 갱신시키는 등의 처리를, 트랜잭션 내에서 안전하게 실행할 수 있습니다. 이를 통해 플레이어의 진행 상황에 맞춘 시기적절한 상품 라인업 제공이 가능해집니다. 또한 "구매 횟수 감소"를 이용하면, 상품의 구매 제한(한정 상품의 재판매 등)을 개별적으로 회복시키는 등의 운영도 쉬워집니다.

## 마스터 데이터 운용

마스터 데이터를 등록하면 마이크로서비스에서 사용 가능한 데이터나 동작을 설정할 수 있습니다.

마스터 데이터의 종류에는 다음과 같은 것이 있습니다.

- `ShowcaseModel`: 스탠다드 진열대의 정의. `DisplayItem`으로서 `SalesItem` 또는 `SalesItemGroup`을 진열할 수 있습니다.
- `RandomShowcaseModel`: 랜덤 진열대의 정의. `RandomDisplayItemModel` 목록에서 `maximumNumberOfChoice` 건을 추첨하여 진열합니다.
- `SalesItem` / `SalesItemMaster`: 단일 판매 상품의 정의.
- `SalesItemGroup` / `SalesItemGroupMaster`: 구매 상황에 따라 진열 내용이 변화하는 상품 그룹의 정의.

마스터 데이터 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub에서 데이터를 반영하거나, GS2-Deploy를 사용해 CI에서 등록하는 등의 워크플로우를 구성할 수 있습니다.

다음은 랜덤 진열대를 포함한 마스터 데이터의 JSON 예시입니다.

```json
{
  "version": "2019-09-13",
  "showcases": [
    {
      "name": "showcase-0001",
      "metadata": "일반 상점",
      "salesPeriodEventId": null,
      "displayItems": [
        {
          "displayItemId": "display-item-0001",
          "type": "salesItem",
          "salesItemName": "item-0001"
        }
      ]
    }
  ],
  "randomShowcases": [
    {
      "name": "random-showcase-0001",
      "metadata": "일일 상점",
      "maximumNumberOfChoice": 4,
      "baseTimestamp": 1700000000000,
      "resetIntervalHours": 24,
      "displayItems": [
        {
          "name": "display-item-0001",
          "weight": 10,
          "stock": 3,
          "consumeActions": [],
          "acquireActions": []
        }
      ]
    }
  ]
}
```

## 구현 예제

### 스탠다드 진열대

#### 진열대 가져오기



**Unity**
```csharp

    var item = await gs2.Showcase.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Showcase(
        showcaseName: "showcase-0001"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Showcase->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Showcase(
        "showcase-0001" // showcaseName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Result = Future->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.showcase.namespace_(
        "namespace-0001"
    ).me(game_session).showcase(
        "showcase-0001"
    )

var async_result = await domain.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


#### 진열대 목록 가져오기

진열대 목록을 가져오는 API는 게임 엔진용 SDK에는 제공되지 않습니다.
가져와야 하는 경우, 관리 콘솔 / GS2 CLI / 각종 언어용 범용 SDK(C# / Go / Python / TypeScript / PHP / Java)를 이용해 주세요.

#### 상품 구매



**Unity**
```csharp

    var result = await gs2.Showcase.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Showcase(
        showcaseName: "showcase-0001"
    ).DisplayItem(
        displayItemId: "display-item-0001"
    ).BuyAsync(
        quantity: 1,
        config: null
    );

    await result.WaitAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Showcase->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Showcase(
        "showcase-0001" // showcaseName
    )->DisplayItem(
        "display-item-0001" // displayItemId
    );
    const auto Future = Domain->Buy(
        1, // quantity
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.showcase.namespace_(
        "namespace-0001"
    ).me(game_session).showcase(
        "showcase-0001"
    ).display_item(
        "display-item-0001"
    )

var async_result = await domain.buy(
    null, # quantity
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


`Buy`는 GS2-Distributor를 경유한 트랜잭션 처리로 실행됩니다. 트랜잭션 결과를 반영하려면, 반환값인 `EzTransactionDomain`의 `WaitAsync` / `Wait`를 호출하여 트랜잭션 완료를 대기해 주세요.

### 랜덤 진열대

#### 진열대 가져오기



**Unity**
```csharp

    var items = await gs2.Showcase.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).RandomShowcase(
        showcaseName: "showcase-0001"
    ).RandomDisplayItemsAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto It = Gs2->Showcase->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->RandomShowcase(
        "showcase-0001" // showcaseName
    )->RandomDisplayItems(
    );
    TArray<Gs2::UE5::Showcase::Model::FEzRandomDisplayItemPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.showcase.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).random_showcase(
        "showcase-0001"
    ).random_display_items(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 오류를 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

```


#### 상품 구매



**Unity**
```csharp

    var result = await gs2.Showcase.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).RandomShowcase(
        showcaseName: "showcase-0001"
    ).RandomDisplayItem(
        displayItemName: "display-item-0001"
    ).RandomShowcaseBuyAsync(
        quantity: 1,
        config: null
    );

    await result.WaitAsync();
```
**Unreal Engine**
```cpp

    const auto Future = Gs2->Showcase->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->RandomShowcase(
        "showcase-0001" // showcaseName
    )->RandomDisplayItem(
        "display-item-0001" // displayItemName
    )->RandomShowcaseBuy(
        1, // quantity
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }
    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.showcase.namespace_(
        "namespace-0001"
    ).me(game_session).random_showcase(
        "showcase-0001"
    ).random_display_item(
        "display-item-0001"
    )

var async_result = await domain.random_showcase_buy(
    1, # quantity
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


#### 랜덤 진열대 상태 가져오기

랜덤 진열대는 플레이어별로 추첨 결과와 구매 횟수를 `RandomShowcaseStatus`로 유지하고 있습니다.
다음 재추첨 시각이나 구매 완료 수를 확인하고 싶은 경우에 가져옵니다.



**Unity**
```csharp

    var item = await gs2.Showcase.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).RandomShowcaseStatus(
        showcaseName: "showcase-0001"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Showcase->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->RandomShowcaseStatus(
        "showcase-0001" // showcaseName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Result = Future->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.showcase.namespace_(
    "namespace-0001"
).me(game_session).random_showcase_status(
    "showcase-0001"
)
var async_result = await domain.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return
var item = async_result.result

```


## 상세 레퍼런스

[GS2-Showcase API 레퍼런스](../../api_reference/showcase)



