忘年会のプレゼント交換で使用するWebサービスを作成した話

## はじめに
この記事は[OIC ITCreate Club Advent Calendar 2019](https://adventar.org/calendars/4713)の25日目の記事です。

## きっかけ
弊部の忘年会の際にプレゼント交換をすることになったのですが、先輩から**「なんか作れない？」**という無茶振りを受け、作成しました。

## 実装
**※忘年会前日に6時間で作成したものです。**

完成したものは[GitHub Pages](https://help.github.com/ja/github/working-with-github-pages/about-github-pages)で公開しています。
[こちら](https://takashi0602.github.io/gacha/)からアクセスできます。

早く作りたかったので使い慣れた[Nuxt.js](https://ja.nuxtjs.org/)で開発しています。また[BootstrapVue](https://bootstrap-vue.js.org/)も使用しています。
詳しく知りたい方は、[リポジトリ](https://github.com/takashi0602/gacha)をご覧下さい。

### 1 ... 人数を入力する画面

急遽キャンセルが出るとまずいので参加人数を入力できるようにします。
参加人数を入力後、スタートボタンを押すと`start()`メソッドが呼び出されます。`start()`メソッド内では、入力された人数が0以下ならエラー文を表示します。また、`data`内で定義した`started`をtureにし、参加人数の入力画面からガチャを回す画面へと移ります。移る前に、`gachaList`へ人数分の長さを持つ配列を代入します。

```pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      <div class="mb-5">
        <p>人数を入力してね！</p>
        <div class="d-flex align-items-end">
          <input type="number" class="form-control w-25 mr-3" v-model="peoples" />
          <div>人</div>
        </div>
        <p v-if="notPeoples" class="text-danger">1人以上入力してください。</p>
      </div>
      <div class="text-center">
        <button class="btn btn-primary px-5 py-2" @click="start">スタート</button>
      </div>
    </form>

  </div>
</template>

<script>
export default {
  data() {
    return {
      peoples: 1,
      notPeoples: false,
      gachaList: [],
      started: false
    }
  },
  methods: {
    start() {
      this.notPeoples = false;
      if (this.peoples <= 0) {
        this.notPeoples = true;
        return;
      }
      for (let i = 1; i <= this.peoples; i++) this.gachaList.push(i);
      this.started = true;
    }
  }
}
</script>

<style scoped>
.c-title {
  font-size: 64px;
}
</style>
```

### 2 ... ガチャを回す画面

スタートボタンが押された後に表示する画面です。
少し凝ったものを作成したかったため、CSSでガチャガチャを作成しました。また、回すボタンを押すとガチャガチャを回すアニメーションも作成しました。呼び出す側(親)から`startAnimation`をガチャガチャコンポーネント(子)へ渡し、それが`true`の場合アニメーションが再生されるように実装しています。
ガチャガチャのCSSに関しては[CodePen](https://codepen.io/takashimelon/pen/zYxzGeV)で公開しましたのでそちらをご確認下さい。

```pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      ...
    </form>

    // 2
    <div v-if="started">
      <div>残りカプセル：{{gachaList.length}}個</div>
      <Gacha :startAnimation="startAnimation" />
      <div class="text-center">
        <button type="button" class="btn btn-primary px-5 py-2" @click="turn">回す</button>
      </div>
    </div>

  </div>
</template>

<script>
import Gacha from "~/components/Gacha"

export default {
  components: {
    Gacha
  },
  data() {
    return {
      ...
      startAnimation: false
    }
  },
  methods: {
    ...
    turn() {
      this.startAnimation = true;
    }
  }
}
</script>
...
```

```components/Gacha.vue
<template>
  <div class="c-gacha">
    <div class="c-gacha-container">
      <div class="c-gacha-box">
        <div class="c-gacha-capsule c-move-1">
          <div class="c-gacha-capsule-color c-red" />
        </div>
        <div class="c-gacha-capsule c-move-2">
          <div class="c-gacha-capsule-color c-blue" />
        </div>
        ...
      </div>
      <div class="c-gacha-body">
        <div class="c-gacha-handle">
          <div v-if="startAnimation" class="c-gacha-handle-bar c-rotate-animation" />
          <div v-else class="c-gacha-handle-bar" />
        </div>
        <div class="c-gacha-exit">
          <div v-if="startAnimation" class="c-gacha-capsule c-show-animation">
            <div class="c-gacha-capsule-color" :style="{background: returnColor()}" />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    startAnimation: {
      default: false
    }
  },
  data() {
    return {
      colorClass: [
        'red',
        'blue',
        'green',
        'yellow'
      ]
    }
  },
  methods: {
    returnColor() {
      return this.colorClass[Math.floor(Math.random() * 4)];
    }
  }
}
</script>

<style scoped>
...
</style>
```

### 3 ... どのプレゼントが当たったかを表示するモーダル

ガチャを回した後にどのプレゼントが当たったかを表示するモーダルです。
※事前にプレゼントに番号を振っておきましょう

回すボタンを押すとランダムな値(0 〜 `gachaList`の長さ - 1の間)を生成します。それを使用しモーダルに表示する数字を決定します。
ガチャガチャのアニメーションが終了してから`openModal()`メソッドを呼び出し、`showModal`を`true`にすることでモーダルを表示しています。また、自分のプレゼントを引いてしまった場合を考慮し、モーダルにはカプセルを戻すボタンも付けました。
ガチャを続けるボタンを押すと`splice()`メソッドを使用し`gachaList[ランダムな値]`を取り除きます。

```pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      ...
    </form>

    // 2
    <div v-if="started">
      ...
    </div>

    // 3
    <modal
      v-if="showModal"
      :number="hitPeopleNumber"
      @returnCapsule="returnCapsule"
      @continueGacha="continueGacha"
    />

  </div>
</template>

<script>
...
import Modal from "~/components/Modal"

export default {
  components: {
    ...
    Modal
  },
  data() {
    return {
      ...
      randomIndex: 0,
      hitPeopleNumber: 0,
      showModal: false
    }
  },
  methods: {
    ...
    turn() {
      ...
      this.randomIndex = Math.floor(Math.random() * this.gachaList.length);
      this.hitPeopleNumber = this.gachaList[this.randomIndex];
      setTimeout(this.openModal, 4000);
    },
    openModal() {
      this.showModal = true;
    },
    returnCapsule() {
      this.startAnimation = false;
      this.showModal = false;
    },
    continueGacha() {
      this.gachaList.splice(this.randomIndex, 1);
      this.startAnimation = false;
      this.showModal = false;
    }
  }
}
</script>
...
```

```components/Modal.vue
<template>
  <div>
    <div class="c-modal">
      <div class="c-modal-content">
        <h3 class="c-modal-title">{{ number }}</h3>
        <div class="c-modal-footer">
          <button class="btn btn-danger mb-3" @click="returnCapsule">カプセルを戻す</button>
          <button class="btn btn-primary mb-3" @click="continueGacha">ガチャを続ける</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    number: {
      default: 1
    }
  },
  methods: {
    close() {
      this.$emit('closeModal');
    },
    returnCapsule() {
      this.$emit('returnCapsule');
    },
    continueGacha() {
      this.$emit('continueGacha');
    }
  }
}
</script>

<style scoped>
.c-modal {
  width: 100vw;
  height: 100vh;
  background-color: rgba(100, 100, 100, 0.4);
  position: fixed;
  top: 0;
  left: 0;
}
.c-modal-content {
  width: 50%;
  height: 500px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  text-align: center;
}
.c-modal-title {
  position: absolute;
  top: 42.5%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 150px;
}
.c-modal-footer {
  display: flex;
  flex-wrap: wrap;
  position: absolute;
  width: 100%;
  justify-content: space-around;
  bottom: 30px;
  padding: 0 20px;
}
</style>
```

### 4 ... 終了を表示するモーダル

ガチャガチャを回し終わった後に表示するモーダルです。
ガチャガチャを人数分回し`gachaList`の長さが0になると、`finished`が`true`となり、終了を表示するモーダルが表示されます。モーダルの閉じるボタンを押すと参加人数を入力する画面へ切り替わります。

```pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      ...
    </form>

    // 2
    <div v-if="started">
      ...
    </div>

    // 3
    <modal
      ...
    />

    // 4
    <finish-modal
      v-if="finished"
      @close="closeFinishModal"
    />

  </div>
</template>

<script>
...
import FinishModal from "~/components/FinishModal"

export default {
  components: {
    ...
    FinishModal
  },
  data() {
    return {
      ...
      finished: false
    }
  },
  methods: {
    ...
    continueGacha() {
      ...
      if (this.gachaList.length === 0) this.finish();
    },
    finish() {
      this.finished = true;
    },
    closeFinishModal() {
      this.started = false;
      this.finished = false;
    }
  }
}
</script>
...
```

```components/FinishModal.vue
<template>
  <div>
    <div class="c-modal">
      <div class="c-modal-content">
        <h3 class="c-modal-title">終了しました！</h3>
        <button class="btn btn-primary px-5 c-modal-btn-primary" @click="close">閉じる</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    close() {
      this.$emit('close');
    }
  }
}
</script>

<style scoped>
.c-modal {
  width: 100vw;
  height: 100vh;
  background-color: rgba(100, 100, 100, 0.4);
  position: fixed;
  top: 0;
  left: 0;
}
.c-modal-content {
  width: 50%;
  height: 500px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  text-align: center;
}
.c-modal-title {
  width: 100%;
  position: absolute;
  top: 42.5%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 50px;
  padding: 0 20px;
}
.c-modal-btn-primary {
  position: absolute;
  bottom: 50px;
  left: 50%;
  transform: translateX(-50%);
}
</style>
```

これで完成です。
再度ですが、[こちら](https://takashi0602.github.io/gacha/)から完成したサービスを確認することができます。

## 最後に
実際の忘年会ではこのWebサービスを使用し、プレゼント交換が楽しく行われました。
参加して下さった方々が楽しそうにしていたので、作成した甲斐があったと思います。