---
title: ZEPETO 마네킹
slug: world-sdk-guide-kr/v62G-zepeto
docTags: 
createdAt: 2024-09-02T08:34:13.134Z
---

ZEPETO 마네킹은 옷을 입어보거나 판매할 수 있는 기능을 지원하는 마네킹 API 패키지입니다.

- 당신이 만든 의류 아이템을 월드에서 판매할 수 있습니다.
- ZEPETO 캐릭터 NPC의 지정된 의상을 입은 마네킹을 만들 수 있습니다.
- 마네킹이나 물체와 상호작용하여 의류 아이템을 구매할 수 있습니다.

::embed[[https://www.youtube.com/watch?v=sag4H7B5oM4](https://www.youtube.com/watch?v=sag4H7B5oM4)]{url="https://www.youtube.com/watch?v=sag4H7B5oM4"}



## 1단계 : 설치

창 → 패키지 관리자 → ZEPETO.World 패키지 1.21.15 이상을 먼저 설치하세요.

그 후, ZEPETO.Module 1.0.8 패키지를 설치하세요.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-TSgCtsqcdWapxQM06E-W2-20240904-102421.png)

:::hint{type="danger"}
**❗️ 주의**

- 성능 개선의 일환으로, 독립형 패키지가 ZEPETO.Module 1.0.8 및 이후 버전 패키지에 통합되었습니다.
  - 이전에 설치된 ZEPETO.Mannequin 패키지를 삭제하세요.
  - 기존 패키지를 제거한 후, ZEPETO.Module 패키지 버전 1.0.8 이상을 설치하세요.
:::



## 2단계 : 마네킹에 설정할 아이템 ID 확인

:::hint{type="success"}
👍 **아이템 ID 확인 방법**

- ZEPETO Studio에서 아이템 제품을 클릭할 때 나타나는 URL을 확인할 수 있습니다.



  ![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-Q1N9VxZNIhBorbjFEqpf0-20240904-102421.png)



- 아이템 ID를 임의의 숫자 형태로 복사하여 Unity Editor에 붙여넣고, 앞에 'CR\_'를 붙입니다.



  ![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-rVU--Xvoc6oq7-8Ie78Zz-20240904-102421.png)
:::

:::hint{type="info"}
**📘&#x20;**&#xC544;이템을 처음 만드는 경우, 다음 가이드를 참조하세요. \[[아이템 만들기](docId\:cQP0hZXKF0dDpQFy6eeY8)]
:::



## 3단계 : 마네킹 설정하기

마네킹 객체는 총 세 가지 방법으로 제작할 수 있습니다.

:::hint{type="danger"}
- 당신이 만든 아이템만 ZEPETO 마네킹으로 설정하고 판매할 수 있습니다.
- 검토에서 거부된 아이템은 판매할 수 없습니다.
- 따라서 이 기능을 사용하려면 Unity에 로그인해야 합니다.
- 마네킹 기능이 포함된 패키지를 게시할 때는 편집자로 로그인해야 합니다.
- ZEPETO 템플릿과 ZEPETO 모델 유형 마네킹을 사용할 때 소비되는 자원은 들어오는 ZEPETO 캐릭터 수에 해당하며, 이는 최적화에 영향을 미칠 수 있습니다.
  - 월드 내에 많은 ZEPETO 캐릭터 모양의 마네킹을 배치해야 하는 경우, 간단한 유형의 마네킹을 선택한 후, [ZEPETO BaseModel](https://studio.zepeto.me/console/download/652892d3a80a9f5505ed888f)을 객체로 활용하는 것이 좋습니다.
:::



### 3단계-1 : 간단한 유형

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N--ZB5MK1nTCo6BIvGniTxd-20240904-102421.png" size="58" width="600" height="600" position="center" caption="간단한 유형 마네킹 사용 예시" showCaption="true"}



가장 기본적인 것입니다. 특정 객체는 마네킹 자식으로 추가하여 외관을 설정할 수 있습니다.

1\) 상호작용할 객체를 추가합니다.

2\) 객체와 상호작용하기 위해서는 충돌체를 추가해야 합니다.

- isTrigger를 체크해야 합니다. 체크하지 않으면 자동으로 트리거로 인식되도록 처리됩니다.
- 충돌체를 추가하지 않고 마네킹 컴포넌트를 추가하려고 하면 아래와 같은 경고 창이 나타납니다.

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-QM-tzQ0BnaRbNa8J9T4No-20240904-102421.png" size="48" width="524" height="582" position="center" showCaption="false"}



3\) 물체에 마네킹 구성 요소를 추가하십시오.

4\) 검사기에서 다음을 설정하십시오.

- 아이콘: 기본적으로 행거 아이콘이 표시됩니다. 사용자 정의 아이콘으로 변경할 수 있습니다.
- 아이콘 위치: 없으면 기본적으로 마네킹 구성 요소와 함께 물체 위치에 아이콘이 나타납니다.
  - 원하는 위치에 나타나도록 위치 객체를 추가한 후 값을 입력하십시오.
- ids: 판매하고자 하는 의류 항목의 아이템 ID를 입력하십시오.

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-M1jGydsLXtPbR1zJCbnom-20240904-102421.png" size="82" width="490" height="635" position="center" showCaption="false"}



5\) 빈 객체를 추가하고 객체 이름을 MannequinScript로 작성하십시오.

6\) ZEPETO 추가 > Typescript를 선택하고 스크립트 이름을 MannequinScript로 작성하세요.

7\) 아래와 같이 작성하세요.

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { ZepetoPlayers, SpawnInfo } from 'ZEPETO.Character.Controller';
import { WorldService } from 'ZEPETO.World';
import { ItemContentsRequest, Mannequin, MannequinComponent, MannequinInteractable, MannequinPreviewer } from 'ZEPETO.Mannequin';
import { Object } from 'UnityEngine';

export default class MannequinScript extends ZepetoScriptBehaviour {

    private _previewer: MannequinPreviewer;

    Start() {
        // 로그인한 ID를 기반으로 ZEPETO 캐릭터를 생성하는 코드  
        // ZepetoPlayers.instance.CreatePlayerWithUserId(WorldService.userId, SpawnInfo.Default, true);
      
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            const character = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
            // Mannequin Interactable Component 추가
            character.gameObject.AddComponent<MannequinInteractable>();
        });

        // 모든 Mannequin 컴포넌트 찾기
        const mannequins = Object.FindObjectsOfType<MannequinComponent>();

        mannequins.forEach(m => {
            // Collider에 들어가기
            m.onActive.AddListener(contents => {
                Mannequin.OpenUI(contents);
                const zepetoContext = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character.Context;
                this._previewer = new MannequinPreviewer(zepetoContext, contents);
                this._previewer.PreviewContents();
            });

            // Collider에서 나가기
            m.onCancel.AddListener(() => {
                Mannequin.CloseUI();
                this._previewer?.ResetContents();
            });
        });
    }
}
```

8\) 객체에 스크립트를 추가한 후 \[▶︎(재생)] 버튼을 눌러 실행하세요.

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-ZB1lyvRY6z-3wb14cMp8Z-20240904-102421.png" size="84" width="846" height="612" position="center" showCaption="false"}



### STEP 3-2 : ZEPETO 모델 유형

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-y2l6LqHS0sK1TBWfzqujO-20240904-102422.png" size="36" width="503" height="740" position="center" caption="ZEPETO 모델 유형 Mannequin 사용 예시" showCaption="true"}



ZEPETO 캐릭터 NPC에 아이템 ID를 추가하여 마네킹의 외관을 설정할 수 있습니다.

1\) 빈 오브젝트를 생성하고 이름을 마네킹으로 지정합니다.

2\) 오브젝트와 상호작용하기 위해서는 콜라이더를 추가해야 합니다.

- isTrigger를 체크해야 합니다. 체크하지 않으면 자동으로 트리거로 인식됩니다.

3\) 오브젝트에 마네킹 구성 요소를 추가합니다.

4\) 인스펙터에서 다음을 설정해 주세요.

- 아이콘: 기본적으로 행거 아이콘이 표시됩니다. 사용자 정의 아이콘으로 변경할 수 있습니다.
- 아이콘 위치: 없으면 기본적으로 마네킹 구성 요소와 함께 오브젝트 위치에 아이콘이 나타납니다.
  - 원하는 위치에 나타나도록 위치 오브젝트를 추가한 후 값을 입력합니다.
- ids: 판매하고자 하는 의류 아이템의 ID를 입력해 주세요.
- 포즈: 포즈를 선택하고 설정할 수 있습니다.
  - 앞으로 원하는 포즈를 포즈 ID 형태로 제공할 예정입니다.



![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-orBhI3SXqNwovbSeU9g0F-20240904-102422.png)



:::hint{type="success"}
**👍&#x20;**&#xC7A5;면에 이미 마네킹 스크립트가 있는 경우, 이후 단계를 건너뛸 수 있습니다.
:::

5\) 빈 오브젝트를 추가하고 오브젝트 이름을 MannequinScript로 작성합니다.

6\) ZEPETO > Typescript를 추가하고 스크립트 이름을 MannequinScript로 작성합니다.

7\) 아래와 같이 작성합니다.

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { ZepetoPlayers, SpawnInfo } from 'ZEPETO.Character.Controller';
import { WorldService } from 'ZEPETO.World';
import { ItemContentsRequest, Mannequin, MannequinComponent, MannequinInteractable, MannequinPreviewer } from 'ZEPETO.Mannequin';
import { Object } from 'UnityEngine';

export default class MannequinScript extends ZepetoScriptBehaviour {

    private _previewer: MannequinPreviewer;

    Start() {
        // 로그인한 ID를 기반으로 ZEPETO 캐릭터를 생성하는 코드  
        // ZepetoPlayers.instance.CreatePlayerWithUserId(WorldService.userId, SpawnInfo.Default, true);
      
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            const character = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
            // 마네킹 상호작용 컴포넌트 추가
            character.gameObject.AddComponent<MannequinInteractable>();
        });

        // 모든 마네킹 컴포넌트 찾기
        const mannequins = Object.FindObjectsOfType<MannequinComponent>();

        mannequins.forEach(m => {
            // 충돌체에 들어가기
            m.onActive.AddListener(contents => {
                Mannequin.OpenUI(contents);
                const zepetoContext = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character.Context;
                this._previewer = new MannequinPreviewer(zepetoContext, contents);
                this._previewer.PreviewContents();
            });

            // 충돌체에서 나가기
            m.onCancel.AddListener(() => {
                Mannequin.CloseUI();
                this._previewer?.ResetContents();
            });
        });
    }
}
```

8\) 객체에 스크립트를 추가한 후 \[▶︎(재생)] 버튼을 눌러 실행합니다.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-AiAykStz2DK3v9iRhC1IN-20240904-102422.png)



### 3단계-3 : ZEPETO 템플릿 유형

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-yrpKiZQT7yT_4S_h7olEe-20240904-102422.png" size="48" width="503" height="740" position="center" caption="ZEPETO 템플릿 유형 마네킹 사용 예시" showCaption="true"}



ZEPETO ID를 입력하면 해당 사람이 입고 있는 옷이 마네킹으로 나타납니다. 그러나 당신이 만든 옷이 아니라면 옷이 표시되지 않습니다.

1\) 빈 객체를 만들고 이름을 마네킹으로 지정합니다.

2\) Collider는 객체와 상호작용하기 위해 추가되어야 합니다.

- isTrigger를 체크해야 합니다. 체크하지 않으면 자동으로 트리거로 인식됩니다.

3\) 객체에 마네킹 구성 요소를 추가하세요.

4\) 인스펙터에서 다음을 설정해 주세요.

- 아이콘: 기본적으로 행거 아이콘이 표시됩니다. 사용자 정의 아이콘으로 변경할 수 있습니다.
- 아이콘 위치: 없으면 기본적으로 마네킹 구성 요소와 함께 객체 위치에 아이콘이 나타납니다.
  - 원하는 위치에 나타나도록 위치 객체를 추가한 후 값을 입력하세요.
- zepeto id: ZEPETO id를 입력해 주세요.
- 포즈: 포즈를 선택하고 설정할 수 있습니다.
  - 앞으로 원하는 포즈를 포즈 id 형태로 제공할 예정입니다.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-qsA1y3SsArpwQW2-kKz49-20240904-102422.png)



:::hint{type="success"}
**👍&#x20;**&#xC774;미 장면에 마네킹 스크립트가 있는 경우, 이후 단계를 건너뛸 수 있습니다.
:::

5\) 빈 객체를 추가하고 객체 이름을 MannequinScript로 작성하세요.

6\) ZEPETO를 추가하고 스크립트 이름을 MannequinScript로 작성하세요.

7\) 아래와 같이 작성하세요.

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { ZepetoPlayers, SpawnInfo } from 'ZEPETO.Character.Controller';
import { WorldService } from 'ZEPETO.World';
import { ItemContentsRequest, Mannequin, MannequinComponent, MannequinInteractable, MannequinPreviewer } from 'ZEPETO.Mannequin';
import { Object } from 'UnityEngine';

export default class MannequinScript extends ZepetoScriptBehaviour {

    private _previewer: MannequinPreviewer;

    Start() {
        // 로그인한 ID를 기반으로 ZEPETO 캐릭터를 생성하는 코드  
        // ZepetoPlayers.instance.CreatePlayerWithUserId(WorldService.userId, SpawnInfo.Default, true);

        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            const character = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
            // Mannequin Interactable Component 추가
            character.gameObject.AddComponent<MannequinInteractable>();
        });

        // 모든 Mannequin 컴포넌트 찾기
        const mannequins = Object.FindObjectsOfType<MannequinComponent>();

        mannequins.forEach(m => {
            // Collider에 들어가기
            m.onActive.AddListener(contents => {
                Mannequin.OpenUI(contents);
                const zepetoContext = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character.Context;
                this._previewer = new MannequinPreviewer(zepetoContext, contents);
                this._previewer.PreviewContents();
            });

            // Collider에서 나가기
            m.onCancel.AddListener(() => {
                Mannequin.CloseUI();
                this._previewer?.ResetContents();
            });
        });
    }
}
```

8\) 객체에 스크립트를 추가한 후 \[▶︎(재생)] 버튼을 눌러 실행하세요.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-xoPRDIgfH-v8tBwV1TtyD-20240904-102422.png)



## 4단계 : 마네킹 구매 사용하기

마네킹 설정을 올바르게 완료하면, 마네킹 객체의 Collider 영역에 들어갈 때 상호작용 아이콘이 나타납니다.

아이콘을 클릭하면 설정한 아이템과 함께 구매 창이 나타납니다.

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-eRDdgTbmEL8FRMK7dLhua-20240904-102422.png" size="80" width="1092" height="803" position="center" showCaption="false"}

아이템을 클릭하면 착용해 볼 수 있습니다.

- 이미 소유하고 있는 아이템은 가격 대신 체크 표시로 표시됩니다.
  맨니킨 객체의 콜라이더 영역을 벗어나면 착용이 취소되고 원래의 의상으로 돌아갑니다.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-2k8Jqwhkyy2aIsCKWtV4_-20240904-102422.png)

다음은 테스트 시나리오에 따른 구매 프로세스 작동 방식입니다.



## STEP 5 : 맨니킨 멀티플레이어 동기화

멀티플레이어는 마네킹 기능을 사용하여 다른 플레이어가 착용한 의상을 동기화해야 합니다.

:::hint{type="success"}
**👍&#x20;**&#xAE30;본 멀티플레이어 설정은 아래 내용을 작업하기 전에 완료되어야 합니다.
:::

1\) 클라이언트 스크립트를 추가합니다. ZEPETO > Typescript를 추가하고 스크립트 이름을 MannequinController로 작성합니다.

2\) 아래와 같이 작성합니다.

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Room } from 'ZEPETO.Multiplay';
import { ClothesPreviewer, ItemContent, ItemContentsRequest, Mannequin, MannequinComponent, MannequinInteractable, MannequinPreviewer } from 'ZEPETO.Mannequin';
import { ZepetoContext } from 'Zepeto';
import { ZepetoPlayer, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { GameObject, Object, Canvas, WaitForSecondsRealtime, LayerMask } from 'UnityEngine';
import { Player } from 'ZEPETO.Multiplay.Schema';
import { ZepetoWorldMultiplay } from 'ZEPETO.World';
 
class CharacterItem {
    property: string;
    id: string;
}
 
class ChangedItem {
    sessionId: string;
    characterItems: CharacterItem[];
}
 
export default class MannequinController extends ZepetoScriptBehaviour {
    private MESSAGE_TYPE = {
        OnChangedItem: "OnChangedItem",
        SyncChangedItem: "SyncChangedItem",
        CheckChangedItem: "CheckChangedItem"
    }
 
    private multiplay: ZepetoWorldMultiplay;
    private _room: Room;
    private _previewer: MannequinPreviewer
    private _context: ZepetoContext
 
    private _userZepetoContexts: Map<string, ZepetoContext> = new Map<string, ZepetoContext>();
    private _currentMannequinComponent: MannequinComponent = null;
    private _selectMannequinComponent: MannequinComponent = null;
    private _isOpenMannequinUI: boolean = false;
 
    private _basicClothString = "basicCloth" as const;
 
    Start() {
        this.multiplay = Object.FindObjectOfType<ZepetoWorldMultiplay>();
 
        Mannequin.OnOpenedShopUI.AddListener((item) => {
            // 상점 버튼을 클릭했을 때
            this.OpenedShopUI(item)
        });
        Mannequin.OnClosedShopUI.AddListener(() => {
            // 닫기 버튼을 클릭했을 때
            this.ClosedShopUI()
        });
        Mannequin.OnSelectedItem.AddListener((itemcontent: ItemContent, select: boolean) => {
            // 아이템이 선택되었을 때의 동작
        });
 
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
 
            // 마네킹
            const character = myPlayer.character;
            character.gameObject.AddComponent<MannequinInteractable>();
 
            console.log("로컬 _context 설정됨");
            this._context = character.Context;
 
            const mannequins = Object.FindObjectsOfType<MannequinComponent>()
            mannequins.forEach(mannequin => {
                // 마네킹 충돌체에 들어갔을 때
                mannequin.onActive.AddListener(contents => {
                    if (this._currentMannequinComponent != null && this._currentMannequinComponent == mannequin) {
                        return;
                    }
 
                    if (contents == null || contents.length == 0) {
                        console.log("마네킹 데이터 없음");
                        return;
                    }
 
                    if (this._isOpenMannequinUI) {
                        this.BreakMannequin();
                    }
 
                    this._selectMannequinComponent = mannequin;
                    Mannequin.OpenUI(contents);
                    console.log("onActive");
                });
 
                // [옵션] 마네킹 충돌체에서 나갔을 때
                mannequin.onCancel.AddListener( () => {
                    if (this._currentMannequinComponent == null || this._currentMannequinComponent != mannequin) {
                        return;
                    }
 
                    this.BreakMannequin();
                    console.log("onCancel");
                });
 
                let iconCanvas = mannequin.gameObject.GetComponentInChildren<Canvas>(true);
                if (iconCanvas != null) {
                    iconCanvas.gameObject.layer = LayerMask.NameToLayer("UI");
                }
            });
        });
 
        ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
            if (ZepetoPlayers.instance.GetPlayer(sessionId).isLocalPlayer) {
                return;
            }
            const userContext = ZepetoPlayers.instance.GetPlayer(sessionId).character.Context;
            this._userZepetoContexts.set(sessionId, userContext);
            this._room.Send(this.MESSAGE_TYPE.CheckChangedItem, sessionId);
        });
        this.multiplay.RoomJoined += (room: Room) => {
            this._room = room;
            this.AddMessageHandler();
        };
    }
 
    private AddMessageHandler() {
        // [옵션] 각 플레이어의 옷을 동기화
        this._room.AddMessageHandler<ChangedItem>(this.MESSAGE_TYPE.SyncChangedItem, message => {
            console.log("syncChangedItem");
            if (message == null) {
                return;
            }
 
            if (false == this._userZepetoContexts.has(message.sessionId)) {
                return;
            }
 
            let itemContents: ItemContent[] = [];
 
            for (const characterItem of message.characterItems) {
                let itemContent: ItemContent = new ItemContent();
                itemContent.id = characterItem.id;
                itemContent.property = parseInt(characterItem.property);
 
                if (itemContent.id == this._basicClothString) {
                    itemContent.id = "";
                }
 
                itemContents.push(itemContent);
            }
            let clothesPreviewer:ClothesPreviewer = new ClothesPreviewer(this._userZepetoContexts.get(message.sessionId),itemContents);
            clothesPreviewer.PreviewContents();
        });
    }
 
    private ClosedShopUI() {
        this._currentMannequinComponent = null;
        this._isOpenMannequinUI = false;
    }
 
    private OpenedShopUI(items: ItemContent[]) {
        this._isOpenMannequinUI = true;
        this._currentMannequinComponent = this._selectMannequinComponent;
 
        this._previewer = new MannequinPreviewer(this._context, items);
        this._previewer.OnChanged.AddListener((changeValues) => {
            let characterItems: CharacterItem[] = [];
 
            for (const changeValue of changeValues) {
                let characterItem: CharacterItem = new CharacterItem();
                characterItem.id = changeValue.id;
                characterItem.property = changeValue.property.toString();
 
                if (characterItem.id == "") {
                    characterItem.id = this._basicClothString;
                }
                characterItems.push(characterItem);
            }
            this._room.Send(this.MESSAGE_TYPE.OnChangedItem, characterItems);
        });
 
        this._previewer.PreviewContents();
    }
 
    public BreakMannequin() {
        Mannequin.CloseUI();
        if (this._previewer) {
            this._previewer.ResetContents();
            this._previewer = null;
        }
 
        this._currentMannequinComponent = null;
        this._isOpenMannequinUI = false;
    }
 
}
```

3\) 계층 구조 만들기 > 빈 객체를 만들고 이름을 마네킹 컨트롤러로 지정합니다.

4\) 방금 작성한 스크립트를 마네킹 컨트롤러에 추가하세요.

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-8MSxbxfTuoV4rKAo-g4_k-20240904-102422.png" size="96" width="452" height="264" position="center" showCaption="false"}



5\) 다음으로, World.Multiplay > index.ts의 내용을 참조하여 서버 코드를 작성하세요.

```typescript
import { Sandbox, SandboxOptions, SandboxPlayer } from 'ZEPETO.Multiplay';
import { Player, Transform, Vector3 } from 'ZEPETO.Multiplay.Schema';
 
class CharacterItem {
    property: string;
    id: string;
}
 
class ChangedItem {
    sessionId: string;
    characterItems: CharacterItem[];
}
 
enum Cloth {
    TOP = "19",
    BOTTOM = "20" ,
    DRESS = "22"
}
 
export default class extends Sandbox {
    private MESSAGE_TYPE = {
        OnChangedItem: "OnChangedItem",
        SyncChangedItem: "SyncChangedItem",
        CheckChangedItem: "CheckChangedItem"
    }
     
    // Map<sessionId, Map<CharacterItem.property, CharacterItem.id>>
    private ChangedItems: Map<string, Map<string, string>>;
 
    onCreate(options: SandboxOptions) {
        // 방 객체가 생성될 때 호출됩니다.
        // 방 객체의 상태 또는 데이터 초기화를 처리합니다.
        this.onMessage("onChangedTransform", (client, message) => {
            const player = this.state.players.get(client.sessionId);
 
            const transform = new Transform();
            transform.position = new Vector3();
            transform.position.x = message.position.x;
            transform.position.y = message.position.y;
            transform.position.z = message.position.z;
 
            transform.rotation = new Vector3();
            transform.rotation.x = message.rotation.x;
            transform.rotation.y = message.rotation.y;
            transform.rotation.z = message.rotation.z;
 
            player.transform = transform;
        });
        this.onMessage("onChangedState", (client, message) => {
            const player = this.state.players.get(client.sessionId);
            player.state = message.state;
            player.subState = message.subState; // 캐릭터 컨트롤러 V2
        });
 
        // 마네킹 서버 코드
        this.ChangedItems = new Map<string, Map<string, string>>();
 
        this.onMessage<CharacterItem[]>(this.MESSAGE_TYPE.OnChangedItem, (client, message) => {
            // 옷을 덮어쓰고 새로운 부품을 설정합니다.
            if (this.ChangedItems.has(client.userId)) {
                const changedItemMap = this.ChangedItems.get(client.userId);
                for (const characterItem of message) {
                    if (characterItem.property == Cloth.DRESS) {
                        // 드레스(22)의 경우, 상의(19)와 하의(20)를 제거합니다.
                        if (changedItemMap.has(Cloth.TOP)) {
                            changedItemMap.delete(Cloth.TOP);
                        }
                        if (changedItemMap.has(Cloth.BOTTOM)) {
                            changedItemMap.delete(Cloth.BOTTOM);
                        }
                    } else if (characterItem.property == Cloth.TOP || characterItem.property == Cloth.BOTTOM) {
                        // 상의(19) 또는 하의(20)인 경우 드레스를 제거합니다.
                        if (changedItemMap.has(Cloth.DRESS)) {
                            changedItemMap.delete(Cloth.DRESS);
                        }
                    }
 
                    changedItemMap.set(characterItem.property,characterItem.id);
                    console.log(`OnChangedItem old ${client.userId} : ${characterItem.property} // ${characterItem.id}`);
                }
            } else {
                // 초기 등록
                let changedItemMap: Map<string,string> = new Map<string, string>();
                for (const characterItem of message) {
                    changedItemMap.set(characterItem.property,characterItem.id);
                }
                this.ChangedItems.set(client.sessionId,changedItemMap);
            }
 
            let changedItem: ChangedItem = new ChangedItem();
            changedItem.sessionId = client.sessionId;
            changedItem.characterItems = message;
 
            console.log(`OnChangedItem : ${changedItem.sessionId}`);
            for (const characterItem of changedItem.characterItems) {
                console.log(` ::: ${characterItem.property} - ${characterItem.id} `);
            }
            this.broadcast(this.MESSAGE_TYPE.SyncChangedItem, changedItem, {except: client});
        });
 
        this.onMessage<string>(this.MESSAGE_TYPE.CheckChangedItem,(client, message) => {
            if (false == this.ChangedItems.has(message)) {
                return;
            }
 
            let changedItem: ChangedItem = new ChangedItem();
            changedItem.sessionId = client.sessionId;
            changedItem.characterItems = [];
 
            for (const property of this.ChangedItems.get(message).keys()) {
                let characterItem: CharacterItem = new CharacterItem();
                characterItem.property = property;
                characterItem.id = this.ChangedItems.get(message).get(property);
 
                changedItem.characterItems.push(characterItem);
            }
 
            client.send<ChangedItem>(this.MESSAGE_TYPE.SyncChangedItem, changedItem );
        });
 
    }
 
    onJoin(client: SandboxPlayer) {
 
        // schemas.json에 정의된 플레이어 객체를 생성하고 초기 값을 설정합니다.
        console.log(`[OnJoin] sessionId : ${client.sessionId}, HashCode : ${client.hashCode}, userId : ${client.userId}`)
 
        const player = new Player();
        player.sessionId = client.sessionId;
      
        if (client.userId) {
            player.zepetoUserId = client.userId;
        }
 
        // 클라이언트 객체의 고유 키 값인 sessionId를 사용하여 플레이어 객체를 관리합니다.
        // 클라이언트는 players 객체에 add_OnAdd 이벤트를 추가하여 추가된 플레이어 객체에 대한 정보를 확인할 수 있습니다.
        this.state.players.set(client.sessionId, player);
    }
 
    async onLeave(client: SandboxPlayer, consented?: boolean) {
 
        // allowReconnection을 설정하면 회로에 대한 연결을 유지할 수 있지만 기본 가이드에서는 즉시 정리합니다.
        // 클라이언트는 players 객체에 add_OnRemove 이벤트를 추가하여 삭제된 플레이어 객체에 대한 정보를 확인할 수 있습니다.
        this.state.players.delete(client.sessionId);
        if (this.ChangedItems.has(client.sessionId)) {
            this.ChangedItems.delete(client.sessionId);
        }
    }
}
```

6\) 멀티플레이어 서버를 켜고 테스트하세요.



## 이벤트 기능

이벤트 기능은 ZEPETO Mannequin 1.1.0부터 사용할 수 있습니다.



### ZEPETO.Mannequin.Mannequin

| **기능**                                                                                     | **설명**                                                                          |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| public static OnSelectedItem: UnityEngine\_Events.UnityEvent$2\<ItemContent, boolean>;     | 마네킹 의상 구매 창에서 특정 항목이 선택될 때 호출되며, 선택된 항목 정보와 항목 선택이 boolean 값으로 전달됩니다.           |
| public static OnSucceededPurchaseItems: UnityEngine\_Events.UnityEvent$1\<ItemContent\[]>; | 의상 구매가 완료되었을 때 호출되며, 구매한 항목 정보의 ItemContent 목록이 전달됩니다.                          |
| public static OnFailedPurchaseItems: UnityEngine\_Events.UnityEvent$1\<ItemContent\[]>;    | 의상 구매가 실패했을 때 호출되며, 실패한 항목의 ItemContent 목록이 전달됩니다.                              |
| public static OnAppliedItems: UnityEngine\_Events.UnityEvent$1\<ItemContent\[]>;           | 의상 구매가 성공적으로 완료된 후 구매한 항목을 착용하기로 선택했을 때 호출되며, 착용한 항목 정보의 ItemContent 목록이 전달됩니다. |
| public static OnOpenedShopUI: UnityEngine\_Events.UnityEvent$1\<ItemContent\[]>;           | 마네킹 의상 구매 창이 열릴 때 호출되며, 구매 창의 항목 정보의 ItemContent 목록이 전달됩니다.                     |
| public static OnClosedShopUI: UnityEngine\_Events.UnityEvent;                              | 마네킹 의상 구매 창이 닫힐 때 호출됩니다.                                                        |



### ZEPETO.Mannequin.BasePreviewer

| **기능**                                                                                                 | **설명**                                                      |
| ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| public OnChanged: UnityEngine\_Events.UnityEvent$1\<ZEPETO\_Mannequin\_BasePreviewer.ChangedValue\[]>; | 장비된 아이템 정보가 마네킹을 눌러 변경될 때 항상 호출되며, OnChagedValue 목록이 전달됩니다. |

OnChagedValue 클래스의 멤버 변수 정보는 다음과 같습니다:

| public property : ZepetoPropertyFlag | 의류 부위 정보 |
| ------------------------------------ | -------- |
| public id : string                   | 아이템 ID   |



### Mannequin.WorldCamera

마네킹.월드카메라가 설정되지 않은 경우, 마네킹 상호작용 아이콘의 카메라는 낮은 깊이의 카메라로 설정됩니다.

특정 카메라를 설정하려면 마네킹.월드카메라를 사용하여 지정할 수 있습니다.

| **변수**    | **설명**                                  |
| --------- | --------------------------------------- |
| 마네킹.월드카메라 | 마네킹 상호작용 아이콘의 카메라를 수동으로 설정할 수 있는 변수입니다. |



아래는 기존 MannequinScript에서 Mannequin.WorldCamera의 설정 예시입니다.

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { ZepetoPlayers, SpawnInfo } from 'ZEPETO.Character.Controller';
import { WorldService } from 'ZEPETO.World';
import { ItemContentsRequest, Mannequin, MannequinComponent, MannequinInteractable, MannequinPreviewer } from 'ZEPETO.Mannequin';
import { Object } from 'UnityEngine';
 
export default class MannequinScript extends ZepetoScriptBehaviour {
     
    private _previewer: MannequinPreviewer;
 
    Start() {
        // 로그인한 ID를 기반으로 ZEPETO 캐릭터를 생성하는 코드 
        // ZepetoPlayers.instance.CreatePlayerWithUserId(WorldService.userId, SpawnInfo.Default, true);
 
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            const character = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
            // Mannequin Interactable Component 추가
            character.gameObject.AddComponent<MannequinInteractable>();
             
            // Mannequin Icon World Canvas Camera를 ZEPETO Camera로 설정하는 코드
            Mannequin.WorldCamera = ZepetoPlayers.instance.LocalPlayer.zepetoCamera.camera;
        });
 
        // 모든 Mannequin 컴포넌트 찾기
        const mannequins = Object.FindObjectsOfType<MannequinComponent>();
 
        mannequins.forEach(m => {
            // Collider에 들어가기
            m.onActive.AddListener(contents => {
                Mannequin.OpenUI(contents);
                const zepetoContext = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character.Context;
                this._previewer = new MannequinPreviewer(zepetoContext, contents);
                this._previewer.PreviewContents();
            });
 
            // Collider에서 나가기
            m.onCancel.AddListener(() => {
                Mannequin.CloseUI();
                this._previewer?.ResetContents();
            });
        });
    }
}
```





## OnContentsLoaded API

Mannequin API를 사용하여 의류 아이템을 장착할 때 모든 의류 아이템이 완전히 로드되면 트리거되는 콜백이 추가되었습니다.

:::CodeblockTabs
API

```typescript
public OnContentsLoaded: UnityEngine_Events.UnityEvent$1<ZEPETO_Mannequin_BasePreviewer.ChangedValue[]>;
```
:::



기존 MannequinScript에서 OnContentsLoaded를 설정하는 예는 다음과 같습니다.

:::CodeblockTabs
MannequinScript

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { ZepetoPlayers, SpawnInfo } from 'ZEPETO.Character.Controller';
import { WorldService } from 'ZEPETO.World';
import { ItemContentsRequest, Mannequin, MannequinComponent, MannequinInteractable, MannequinPreviewer } from 'ZEPETO.Mannequin';
import { Object } from 'UnityEngine';
 
export default class MannequinScript extends ZepetoScriptBehaviour {
 
    private _previewer: MannequinPreviewer;
 
    Start() {
        // 로그인한 ID를 기반으로 ZEPETO 캐릭터를 생성하는 코드 
        // ZepetoPlayers.instance.CreatePlayerWithUserId(WorldService.userId, SpawnInfo.Default, true);
 
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            const character = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
            // Mannequin Interactable Component 추가
            character.gameObject.AddComponent<MannequinInteractable>();
        });
 
        // 모든 Mannequin 컴포넌트 찾기
        const mannequins = Object.FindObjectsOfType<MannequinComponent>();
 
        mannequins.forEach(m => {
            // Collider에 들어가기
            m.onActive.AddListener(contents => {
                Mannequin.OpenUI(contents);
                const zepetoContext = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character.Context;
                this._previewer = new MannequinPreviewer(zepetoContext, contents);
 
                // OnContentsLoaded 이벤트에 리스너 추가
                this._previewer.OnContentsLoaded.AddListener((changedValues)=>{
                    console.log('Contents loaded:', changedValues);
                });
 
                this._previewer.PreviewContents();
            });
 
            // Collider에서 나가기
            m.onCancel.AddListener(() => {
                Mannequin.CloseUI();
                this._previewer?.ResetContents();
            });
        });
    } 
}
```
:::

