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

# GS2-Dictionary

도감 기능




GS2-Dictionary 에서는 게임 내에서 입수한 아이템이나 캐릭터의 도감 기능을 구현합니다.

기본적으로 GS2-Inventory 의 단순화된 구현 버전이라고 생각하시면 되며, 입수 완료/미입수라는 2값의 소지 상태를 관리할 수 있습니다.

도감 이외의 용도로도 사용할 수 있습니다. 예를 들어 아바타 파츠의 소지 상태가 좋은 예입니다.
아바타 파츠를 가지고 있는지 여부는 파츠별로 2값으로 상태를 관리할 수 있으면 충분하므로 GS2-Dictionary 에 적합합니다.
그 외에도 완료된 튜토리얼 단계 관리, 해금된 컷신 관리, 해금된 BGM 목록 등 2값 상태로 표현할 수 있는 요소 전반에 활용할 수 있습니다.

```mermaid
graph TD
  Boss["보스를 토벌"] -- 보상으로 엔트리 추가 --> Dictionary["GS2-Dictionary 에 기록"]
  Shop["상점에서 구매"] -- 보상으로 엔트리 추가 --> Dictionary
  Dictionary --> Browse["플레이어가 도감을 열람"]
  Dictionary -- 소지 증명 서명 --> Formation["GS2-Formation 등에서 이용"]
```

## GS2-Inventory 와의 차이점

### 스택 개념의 유무

GS2-Inventory 는 동일한 아이템을 여러 개 소지할 때 스택이라는 개념이 있습니다.
이는 포션을 최대 99개까지 스택할 수 있고, 99개를 초과하면 두 번째 스택을 생성하는 것과 같은 사양입니다.

이 사양이 있기 때문에 GS2-Inventory 에서는 "포션의 소지 수량을 조회한다"는 API 의 반환값이 리스트 형태입니다.
이는 포션이 여러 스택으로 존재할 가능성이 있기 때문입니다.
이 사양은 포션과 같은 데이터를 관리하는 데는 편리하지만, 도감과 같은 단순한 소지 상태를 관리하기에는 과도한 사양이며, 리스트를 다루면 코드량도 늘어납니다.

### 입수 처리로 여러 엔트리를 등록 가능

GS2-Inventory 는 포션 입수와 엘릭서 입수를 각각 별도의 API 요청으로 처리해야 합니다.
GS2-Dictionary 는 한 번의 API 요청으로 최대 100개의 엔트리를 등록할 수 있습니다.

이를 통해 퀘스트 클리어 보상으로 "쓰러뜨린 몬스터 전종을 한꺼번에 도감에 등록"과 같은 처리를 한 번의 요청으로 효율적으로 수행할 수 있습니다.

### 엔트리의 검증과 삭제

GS2-Dictionary 에서는 한 번 기록한 엔트리를 삭제하거나, 특정 엔트리를 입수했는지(또는 미입수인지) 검증할 수 있습니다. 기간 한정 이벤트 아이템의 소지 상태를 초기화하거나, 특정 아이템을 입수한 경우에만 퀘스트를 시작할 수 있도록 제한하는 등의 운영이 가능합니다.

### 기능 비교

| 항목 | GS2-Dictionary | GS2-Inventory |
| -- | -- | -- |
| 상태 관리 | 소지/미소지의 2값 | 수량·스택 관리 |
| 1회 요청당 등록 수 | 최대 100건 | 1건씩 |
| 즐겨찾기 기능 | 있음 | 없음 |
| 소지 증명 서명 | 있음 | 있음(ItemSet 단위) |
| 주요 용도 | 도감, 아바타 파츠, 해금 상태 | 일반적인 아이템 소지 |

## 마스터 데이터(EntryModel)

도감에 등록 가능한 엔트리를 마스터 데이터로 정의합니다.
`EntryModel` 의 주요 설정 항목은 다음과 같습니다.

| 항목 | 설명 |
| -- | -- |
| `name` | 엔트리 이름(플레이어별 기록 키) |
| `metadata` | 클라이언트에서 이용하는 임의의 메타데이터(표시 이름, 이미지 참조, 희귀도 등) |

마스터 데이터의 JSON 예:

```json
{
  "version": "2020-04-30",
  "entryModels": [
    {
      "name": "monster-0001",
      "metadata": "{\"displayName\":\"슬라임\",\"rarity\":1}"
    },
    {
      "name": "monster-0002",
      "metadata": "{\"displayName\":\"고블린\",\"rarity\":2}"
    }
  ]
}
```

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

## 즐겨찾기 기능

GS2-Dictionary 에서는 기록된 엔트리에 "즐겨찾기"를 지정할 수 있습니다. `AddLikes` 나 `DeleteLikes` 로 즐겨찾기 목록을 관리하고, `Likes` API 로 목록 조회나 변경 알림 구독이 가능합니다. 즐겨찾기 정보를 활용하면 도감 화면의 필터링이나 UI 에서의 우선 표시를 구현할 수 있습니다.

즐겨찾기 엔트리는 내부적으로 `LikeToc` 라는 단위로 정리되어 저장되므로, 한 사용자가 대량의 즐겨찾기를 등록하더라도 효율적으로 조회할 수 있습니다.

## 소지 증명 서명

GS2-Dictionary 는 엔트리의 소지 상태에 대해 서명된 데이터를 발급할 수 있습니다.
다른 GS2 마이크로서비스와 연동할 때, 서버 간 통신을 거치지 않고도 "실제로 해당 엔트리를 소지하고 있음"을 보증할 수 있습니다.

```mermaid
sequenceDiagram
  participant Player
  participant Dictionary as GS2-Dictionary
  participant Formation as GS2-Formation
  Player ->> Dictionary: GetEntryWithSignature(entry, keyId)
  Dictionary -->> Player: Body + Signature
  Player ->> Formation: SetForm(itemId, body, signature)
  Formation ->> Formation: 서명 검증
  Formation -->> Player: 성공
```

검증하는 측의 서비스는 `body` 안에 포함된 정보(엔트리 이름이나 사용자ID)를 `signature` 를 사용하여 검증할 수 있습니다.

## 스크립트 트리거

네임스페이스에 `entryScript` 나 `duplicateEntryScript` 를 설정하면 엔트리 등록 전후나 중복 등록 시에 커스텀 스크립트를 호출할 수 있습니다. 스크립트는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기에서는 GS2-Script 나 Amazon EventBridge 를 이용한 외부 연동도 가능합니다.

설정 가능한 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.

- `entryScript`(완료 알림: `entryDone`): 엔트리 등록 전후
- `duplicateEntryScript`: 이미 등록된 엔트리를 재등록했을 때

`duplicateEntryScript` 를 이용하면, 예를 들어 "이미 도감에 등록된 희귀 몬스터를 다시 드롭한 경우에는 대신 다른 통화를 지급한다"와 같은 전환 처리를 구현할 수 있습니다.

## 트랜잭션 액션

GS2-Dictionary 에서는 다음과 같은 트랜잭션 액션을 제공합니다.

- 검증 액션: 엔트리 소지 상황 검증
- 소비 액션: 엔트리 삭제
- 입수 액션: 엔트리 추가

"엔트리 소지 상황 검증"을 검증 액션으로 이용하면, 특정 몬스터를 토벌 완료한(도감에 등록된) 플레이어만 도전할 수 있는 퀘스트나, 특정 아이템을 한 번이라도 입수한 적이 있는 플레이어만 구매할 수 있는 상품과 같은 제한을 설정할 수 있습니다.

### 도감에 기록하는 방법

도감에 대한 기록(`Entry` 추가)은 게임 엔진용 SDK 에서 직접 호출할 수 없습니다.
GS2 의 보안 모델상, 클라이언트에서 일방적으로 변경하는 것을 금지하고 서버가 신뢰하는 경로에서만 추가되도록 설계되어 있기 때문입니다.

구체적으로는 다음과 같은 형태로 트랜잭션 액션에 포함시켜 사용합니다.

- GS2-Quest 의 퀘스트 클리어 보상으로 "쓰러뜨린 몬스터를 도감에 등록"하는 입수 액션을 설정
- GS2-Showcase 에서 구매한 아이템의 보상으로 "해당하는 엔트리를 도감에 등록"하는 입수 액션을 설정
- GS2-Mission 의 미션 달성 보상으로 "특정 엔트리를 등록"하는 입수 액션을 설정

## 구현 예제

### 도감에 기록 가능한 마스터 데이터 목록 조회



**Unity**
```csharp

    var items = await gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).EntryModelsAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    );
    const auto It = Domain->EntryModels(
    );
    TArray<Gs2::UE5::Dictionary::Model::FEzEntryModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.dictionary.namespace_(
        "namespace-0001"
    ).entry_models(
    )

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 items = await gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).EntriesAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto It = Domain->Entries(
    );
    TArray<Gs2::UE5::Dictionary::Model::FEzEntryPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.dictionary.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).entries(
    )

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 item = await gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Entry(
        entryModelName: "entry-0001"
    ).ModelAsync();

    var acquiredAt = item.AcquiredAt;
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Entry(
        "entry-0001" // entryModelName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Result = Future->GetTask().Result();
    const auto AcquiredAt = Result->GetAcquiredAt();
```
**Godot**
```gdscript

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

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

var result = async_result.result

```


### 소지 증명 서명 조회

GS2 내의 다른 마이크로서비스와 연동할 때, 실제로 GS2-Dictionary 에 엔트리가 기록되어 있는지 보증된 데이터를 요구받는 경우가 있습니다.

예를 들어, GS2-Dictionary 로 아바타 파츠의 소지 상태를 관리하고, GS2-Formation 으로 아바타 파츠의 편성 상태를 관리한다고 가정해 봅시다.
GS2-Formation 에 헤어스타일을 설정할 때 "hair-0001"이라는 파츠를 설정하도록 API 요청을 보내게 되는데,
GS2-Formation 은 소지 증명 서명과 함께 "hair-0001"을 지정하도록 요구합니다.

이를 통해 GS2-Formation 은 뒤에서 GS2-Dictionary 와 통신하여 실제로 소지하고 있는 파츠인지 판단할 필요가 없어집니다.



**Unity**
```csharp

    var result = await gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Entry(
        entryModelName: "entry-0001"
    ).GetEntryWithSignatureAsync(
        keyId: "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001"
    );

    var body = result.Body;
    var signature = result.Signature;
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Entry(
        "entry-0001" // entryModelName
    );
    const auto Future = Domain->GetEntryWithSignature(
        "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001" // keyId
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

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

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

var async_result = await domain.get_entry_with_signature(
    "key-0001" # key_id
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 즐겨찾기 엔트리 등록



**Unity**
```csharp

    var domain = gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    );
    var result = await domain.AddLikesAsync(
        entryModelNames: new List<string> {
            "entry-0001",
        }
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->AddLikes(
        {
            "entry-0001",
        }
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

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

var async_result = await domain.add_likes(
    [
        "entry-0001",
        "entry-0002",
        "entry-0003",
    ] # entry_model_names
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 즐겨찾기 엔트리 목록 조회



**Unity**
```csharp

    var domain = gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    );
    var items = await domain.LikesAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto It = Domain->Likes(
    );
    TArray<Gs2::UE5::Dictionary::Model::FEzLikePtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.dictionary.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).likes(
    )

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 domain = gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    );

    await domain.DeleteLikesAsync(
        entryModelNames: new List<string> {
            "entry-0001",
        }
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );

    const auto Future = Domain->DeleteLikes(
        {
            "entry-0001",
        }
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

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

var async_result = await domain.delete_likes(
    [
        "entry-0001",
        "entry-0002",
        "entry-0003",
    ] # entry_model_names
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


## 상세 레퍼런스

[GS2-Dictionary API 레퍼런스](../../api_reference/dictionary)



