# 게임 경매장과 플레이어 장터

> 등록, 입찰, 카운트다운을 모든 클라이언트에 실시간으로 보여 주고, 마감 직전 입찰은 자동 연장으로 스나이핑을 막으며, 즉시 구매와 낙찰가 추이를 지원합니다. 입찰 판단은 여러분의 서버가, 재화와 아이템은 게임이 관리합니다.

Source: https://hibira.io/ko/use-cases/game-auction-house

경매장, 거래소, 플레이어 장터는 게임 경제를 돌아가게 하지만, 모두가 같은 것을 같은 순간에 볼 때만 공정하게 느껴집니다. 누군가 입찰하는 순간 새 가격이 보이고, 모든 화면의 카운트다운이 같고, 마지막 1초에 빼앗기지 않아야 합니다. 오픈 소스 [@hibira-io/auction](https://www.npmjs.com/package/@hibira-io/auction) 패키지는 이를 Hibira 위에서 제공하며, 모든 입찰은 여러분의 서버가 판단합니다.

## 플레이어가 얻는 것

- 등록, 가격 변화, 종료가 새로고침 없이 열려 있는 모든 클라이언트에 반영됩니다.
- 카운트다운은 서버 시계에 맞춰 흐르고, 마감 직전 입찰은 스나이핑이 아니라 시간 연장이 됩니다.
- 더 높은 입찰이 들어온 순간의 알림, 즉시 구매, 아이템별 최근 낙찰가.

![경매장 예제: 한 플레이어가 사냥꾼의 활에 입찰하고 곧 다른 창에서 더 높게 입찰하면, 첫 플레이어에게 새 가격과 알림이 표시됩니다.](https://hibira.io/portal/articles/demo-game-auction-house-ko.webp)

## 사용한 기능

- **경매장마다 공개 또는 비공개 채널 하나**(`auction-main`). 이벤트는 `auction-update` 하나뿐이며, 등록 물품 전체와 버전 번호를 담습니다.
- **서버에서 호출하는 HTTP API**(`trigger`). 보는 플레이어가 몇 명이든 변경마다 한 번만 게시합니다.
- **[@hibira-io/auction](https://github.com/hibira-io/auction-js)**: 서버 쪽은 입찰을 검사하고, 마감 직전 경매를 연장하고, 낙찰을 처리합니다. 클라이언트 쪽은 물품마다 최신 버전을 유지하고 재연결 뒤 따라잡습니다.

```text
sequence: 입찰은 내 서버로, 알림은 Hibira가 모든 플레이어에게
플레이어 A | 내 서버 | *Hibira | 플레이어 B
1 > 2: POST /auction/:id/bid | 120 골드
note: house.bid()가 입찰을 검사하고 골드를 묶어 둠
2 > 3: auction-update | 가격 120, A 선두
3 > 1: auction-update
3 > 4: auction-update | "입찰 126"
```

## 1단계: 서버에서 경매장 만들기

아이템과 재화는 계속 게임이 관리합니다. 경매장은 입찰자의 재화를 묶어 두고, 더 높은 입찰이 오면 돌려주고, 경매가 끝나면 아이템과 재화를 옮기도록 여러분에게 요청합니다.

```js
import Pusher from 'pusher';
import { createAuctionHouse } from '@hibira-io/auction/server';

const hibira = new Pusher({ appId, key, secret, host: 'ws.hibira.io', useTLS: true });

const house = createAuctionHouse({
  pusher: hibira,
  name: 'main',
  extendWithin: 30000, // a bid in the last 30 s adds time
  reserve: async ({ userId, amount }) => {
    if (!(await wallet.hold(userId, amount))) throw new Error('not enough gold');
  },
  release: async ({ userId, amount }) => wallet.unhold(userId, amount),
  settle: async ({ listing }) => game.deliver(listing), // idempotent by listing.id
});
house.start(); // closes and settles ended auctions
```

## 2단계: 엔드포인트 네 개

플레이어는 요청 본문이 아니라 반드시 여러분의 세션에서 가져오세요.

```js
app.get('/auction', async (req, res) => res.json(await house.snapshot()));

app.post('/auction/:id/bid', requireLogin, async (req, res) => {
  try {
    res.json(await house.bid({ listingId: req.params.id, bidderId: req.user.id, bidder: { name: req.user.name }, amount: req.body.amount }));
  } catch (err) {
    res.status(err.status ?? 500).json({ error: err.code });
  }
});
// The same for house.buyout() and house.list()
```

## 3단계: 게임에 경매장 표시하기

```js
import { AuctionHouse } from '@hibira-io/auction';

const house = AuctionHouse.open(pusher, 'main', {
  load: () => fetch('/auction').then((r) => r.json()),
});

house.on('change', ({ listings }) => render(listings));
setInterval(() => listings.forEach((l) => showTimeLeft(l, house.timeLeft(l))), 250);

bidButton.onclick = () => fetch(`/auction/${l.id}/bid`, { method: 'POST', body: JSON.stringify({ amount: l.minBid }) });
```

`house.prices(type)`는 아이템 종류별 최근 낙찰가를 돌려주므로 가격 차트에 쓸 수 있습니다. 재연결 뒤에는 스냅샷을 다시 불러오므로 놓치는 변경이 없습니다.

## 직접 해 보기

[경매장 예제](https://github.com/hibira-io/examples/tree/main/nextjs-auction)는 NPC 상인, 가방, 즉시 구매, 입찰 알림, 낙찰가를 갖춘 완전한 Next.js 게임 장터이며 7개 언어를 지원합니다. 두 창에서 열고 한쪽에서 입찰해 보세요.

## 팁

- 보는 플레이어가 몇 명이든 변경 한 번은 메시지 하나입니다. 큰 경매장은 카테고리나 지역으로 나누면(`name: 'weapons'`) 각 클라이언트가 보여 줄 부분만 불러옵니다.
- 게임 서버가 여러 대라면 데이터베이스 위의 `store`를 넘기세요. 저장은 비교 후 쓰기라서 두 서버가 같은 입찰을 함께 받아들일 수 없습니다.
- 옮기는 것은 게임 안의 재화와 아이템이며, 실제 돈은 다루지 않습니다.
- Unity, Godot, 그 밖의 Pusher SDK 클라이언트도 같은 `auction-main` 채널을 구독하고 같은 엔드포인트를 호출할 수 있습니다. 방과 매치메이킹은 [멀티플레이어 게임 방과 로비](https://hibira.io/ko/use-cases/game-lobbies)를 참고하세요.
