【Cybozu Days 2021】kintone GAME Laboの裏側 ~受付システム編~

# はじめに

[kintone Advent Calendar 2021](https://qiita.com/advent-calendar/2021/kintone) 25日目の記事です。

今年もkintoneアドベントカレンダーにはたくさんの記事が書かれました！ありがたい限りです！
そして僕はいつも通りぎりぎりの公開ですw
しかも今年は同じ日に2記事書くという暴挙！完全に自分の首を絞めて失敗したなと思っています。。

今年のCybozu Days 2021 のゲームブースについて、技術部分はQiitaに書くよ、とnoteの方で説明しちゃったのでしっかり技術部分メインで書きたいと思います。
 
https://note.com/ryotkbb/n/na8bc22fb8037

もう1本の方はこちら

https://qiita.com/RyBB/items/0310ae89b76aef57da78

# おさらい：kintone GAME Labo とは

今年2021年の11月1日、2日に幕張メッセで行われたサイボウズのイベント [Cybozu Days 2021](https://days.cybozu.co.jp/days/) のブースです。kintoneを使ったゲームが体験できるブースとして、いろいろ作りました。
![PXL_20211102_035305281_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/e3c4b527-ea37-c286-b118-a68e5d8a5c85.jpeg)

# 受付システムとは

今回、ブースでは独立した3つのゲームを用意していて、それらをまとめる共通のWebシステムとして受付システムを用意しました。
受付システムでは

- ユーザー登録
- QRコード表示
- 自身のスコア管理
- 全プレイヤーのスコアランキング

という機能を作りました。

ここだけの話、最初はkintone連携サービスであるトヨクモさんのForm BridgeやkViewerを使えばいけそうかなって思ってましたが、ブラウザ閉じたあとのQRコードの表示どうしようとかいろいろ考えているうちに

> まぁ自分で一から作ればいいか

となりました。
（本当は連携サービス使ったほうがエコシステム的な観点でも良かったのですが・・・無念・・・）

# 利用した機能

受付システムはWebシステムとして作成しています。フロントエンドはVue.js、バックエンドはAWSといった感じです。ホスティングはGitHub Pagesでさくっとやってました。

- Vue.js
- AWS
    - API Gateway
    - Lambda
    - Dynamo DB
- GitHub Pages

![Untitled Diagram.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/815fbfd3-aca2-3171-5414-e20425fae9be.png)

# 動作イメージ

トップ画像
![CD2021受付_トップ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/b463cb83-8183-4dd5-b63d-a405bafec607.gif)
サイバーパンクっぽくネオン色でヒュンヒュンしてますw

全体の動き
![CD2021受付_動き.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/4ae4d788-490d-1651-d8c6-9fdfb603590a.gif)
カーソルのマウスオーバー時とか戻るボタンとか地味なところまでこだわり強めに作ってますw

# 注力ポイント

今回の受付システムはなんといっても見た目を一番こだわりました。ブースのイメージが**サイバーパンク**だったので、それに合わせてネオン調の色合いだったりフォントだったりこだわり全開です！

フォントはこのサイトからいろいろを探して使ってみました。スタイリッシュというか ~~中二病~~ かっこいいフォントが多くて今回のイメージにピッタリでした！

> ▶ FONTGET
> https://www.fontget.com/

色合いについては、蛍光色を1つメインカラーとして決めて、それを `hue-rotate()` で色相変更をして使ってます。
hue-rotateを使えば勝手に同じ色合いで色違いが作れるのでめっちゃ便利でした！

![スクリーンショット 2021-12-25 16.41.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/6eb5fa23-19cc-7c65-222a-efff0c4f5656.png)

```css
#app {
  color: #03e9f4;
}
.text1 {
  filter: hue-rotate(0deg);
}
.text2 {
  filter: hue-rotate(90deg);
}
.text3 {
  filter: hue-rotate(180deg);
}
.text4 {
  filter: hue-rotate(270deg);
}
```

# プログラム：Vue.js

全部載せると多すぎるのでピックアップしてご紹介。
今回はトップページでどのメニューをクリックするかでページ遷移もしたかったので、Vue.jsのRouterにも初挑戦してみました。

> ▶ Vue Router
> https://router.vuejs.org/ja/
> Vue.jsを利用したSPA構築でルーティング制御をするための機能。

## トップページ

```vuejs:Index.vue
<template>
  <div class="cd2021-bb-body">
    <div class="cd2021-bb-btn" @click="router('/entry')">
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      sign up
    </div>
    <div class="cd2021-bb-btn" @click="router('/qrcode')">
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      QR code
    </div>
    <div class="cd2021-bb-btn" @click="router('/mypage')">
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      my page
    </div>
    <div class="cd2021-bb-btn" @click="router('/ranking')">
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      ranking
    </div>
  </div>
</template>
<script>
export default {
  name: "cd2021-bb-body",
  methods: {
    router(path) {
      this.$router.push(path);
    },
  },
};
</script>

<style lang="scss">
.cd2021-bb-body {
  background-color: #000;
  height: 100vh;
  text-align: center;
}
.cd2021-bb-btn {
  position: relative;
  font-family: "suggested";
  font-size: calc(min(8vw, 5rem));
  margin: 5vh 0;
  padding: 5vh 0;
  color: var(--main-color);
  text-decoration: none;
  text-transform: uppercase;
  transition: 0.5s;
  letter-spacing: 5px;
  overflow: hidden;
  cursor: pointer;
  &:hover {
    background: var(--main-color);
    color: #050801;
    box-shadow: 0 0 5px var(--main-color), 0 0 25px var(--main-color),
      0 0 50px var(--main-color), 0 0 200px var(--main-color);
    -webkit-box-reflect: below 1px linear-gradient(transparent, #0005);
  }
  &:nth-child(1) {
    filter: hue-rotate(0);
  }
  &:nth-child(2) {
    filter: hue-rotate(90deg);
  }
  &:nth-child(3) {
    filter: hue-rotate(180deg);
  }
  &:nth-child(4) {
    filter: hue-rotate(270deg);
  }
  span {
    position: absolute;
    display: block;
    &:nth-child(1) {
      top: 0;
      left: 0;
      width: 100%;
      height: 2px;
      background: linear-gradient(90deg, transparent, var(--main-color));
      animation: animate1 2s linear infinite;
    }
    &:nth-child(3) {
      bottom: 0;
      right: 0;
      width: 100%;
      height: 2px;
      background: linear-gradient(270deg, transparent, var(--main-color));
      animation: animate2 2s linear infinite;
    }
  }
}
@keyframes animate1 {
  0% {
    left: -100%;
  }
  50%,
  100% {
    left: 100%;
  }
}
@keyframes animate2 {
  0% {
    right: -100%;
  }
  50%,
  100% {
    right: 100%;
  }
}
</style>
```

ヒュンヒュン動くアニメーションはこの記事を ~~パクっ~~ 参照しました！

> ▶ CSSで美しいネオンライトのエフェクトをテキストやボタン、ボーダーに実装するテクニックのまとめ
> https://coliss.com/articles/build-websites/operation/css/css-neon-sign-effects.html

## ユーザー登録ページ

ユーザー登録をするとkintoneへレコードが新規作成されるようになっていて、レスポンスであるレコードIDをいろいろなところで使うようにしています。ユーザー登録をするとブラウザのセッションストレージにレコードIDが保存されるようにしていて、基本的には何も意識しなくても自分のユーザー情報がわかるようにちょっとこだわってみました。

```vuejs:Signup.vue
<template>
  <div class="cd2021-bb-signup">
    <div class="back"><a href="/CD2021_RECEPTION">back </a></div>
    <div class="cd2021-bb-signup-form">
      <label>User Name</label>
      <input v-model="name" placeholder="名前" />
      <div class="cd2021-bb-button" @click="signup(name)">sign up</div>
    </div>
    <div class="cd2021-bb-signup-yourid" v-if="isBeforeShownId">
      {{ name }} さんの ID は...
    </div>
    <div class="cd2021-bb-signup-yourid" v-if="isShownId">
      {{ name }} さんの ID は<span class="your-id">{{ id }}</span
      >です
    </div>
  </div>
</template>

<script>
export default {
  name: 'signup',
  data() {
    return {
      name: '',
      id: '',
      isBeforeShownId: false,
      isShownId: false,
      isSignup: false,
    };
  },
  methods: {
    async signup(name) {
      try {
        if (!name) {
          this.$toasted.error('表示名を入力してください', {
            position: 'bottom-right',
            duration: 2000,
            fullWidth: true,
          });
          return;
        }
        if (this.isSignup) {
          this.$toasted.error(
            '一度登録されています。再度登録する際は画面をリロードしてください。',
            {
              position: 'bottom-right',
              duration: 2000,
              fullWidth: true,
            },
          );
          return;
        }
        this.isBeforeShownId = true;
        const resp = await fetch(AWS_URL, {
          method: 'POST',
          mode: 'cors',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ name }),
        });
        const res = await resp.json();
        this.isBeforeShownId = false;
        this.isShownId = true;
        this.id = res.id;
        this.isSignup = true;

        // セッションストレージに保存
        sessionStorage.setItem('id', res.id);
      } catch (err) {
        console.error(err);
      }
    },
  },
};
</script>

<style lang="scss">
.cd2021-bb-signup {
  color: var(--main-color);
  filter: hue-rotate(0);
}

.cd2021-bb-signup-form {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 30vh;
  font-size: 3rem;
  input {
    color: #fff;
    font-size: 3rem;
    width: 30vw;
    text-align: center;
    border: none;
    background-color: #000;
    border-bottom: 1px solid var(--main-color);
    outline: none;
    padding-bottom: 8px;
    margin: 0 2rem;
  }
}

.cd2021-bb-signup-yourid {
  margin: 50px 0 0 0;
  font-size: 3rem;
  text-align: center;
  .your-id {
    font-size: 5rem;
    font-weight: bold;
    margin: 0 1rem;
    filter: hue-rotate(90deg);
  }
}

@media (max-width: 767px) {
  .cd2021-bb-signup-form {
    flex-flow: column;
    text-align: center;
    input {
      font-size: 3rem;
      height: 4.5rem;
      width: 50vw;
      margin: 5rem;
    }
  }
}
</style>
```

# プログラム：AWS Lambda

ここはいつも通りというか、普通にkintoneに対してリクエストを投げてます。
今回でだいぶrest-api-clientにも慣れて、ドキュメントなしで使えるようになりましたw

```signup.js
"use strict";
const { KintoneRestAPIClient } = require("@kintone/rest-api-client");

const client = new KintoneRestAPIClient({
  baseUrl: `https://${process.env.KINTONE_DOMAIN}`,
  auth: {
    username: process.env.KINTONE_USERNAME,
    password: process.env.KINTONE_PASSWORD,
  },
});

const callback = (statusCode, body) => {
  return {
    statusCode,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
    body: JSON.stringify(body),
  };
};
module.exports.hello = async (event) => {
  try {
    console.log(event.body);
    if (!event.body) throw new Error("パラメータを指定してください。");
    const body = JSON.parse(event.body);

    const res = await client.record.addRecord({
      app: process.env.KINTONE_USERMANAGE_APPID,
      record: {
        ユーザー名: {
          value: body.name,
        },
      },
    });
    return callback(200, { id: res.id });
  } catch (err) {
    console.error(err);
    return callback(500, err);
  }
};
```

## API化

今回、それぞれのゲームからも得点データをkintoneにアップする必要があり、それぞれがそれぞれでリクエスト処理を書くのはだるいかなと思ってAPI GatewayとLambdaの処理をAPI化しました。

といっても、ドキュメントを作って誰でも使えるようにしただけなんですけどねw

![スクリーンショット 2021-12-25 17.13.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/8093461f-361a-dc02-f4c4-a38ed18f79ff.png)

処理内容を修正する場合もリクエストパラメータの内容さえ同じにしておけば、自由に修正ができて全てに反映されるので、思った以上に便利でした！

# おわりに

ということで、今年のDaysのゲームブースの受付はこんな感じに作成していました。
本当に１から全部つくったのでまぁまぁの規模のソースコードになってます。当然リファクタリングなんてできてませんw

Vue.js + SCSS + Lambda + GitHub Pages は自分の中でのテンプレになってるくらいよく使ってます。
（去年のアドベントカレンダーでも `Vue.jsは手軽でいいぞ〜`でしめてたw）

もう1本の方もぜひ読んでみてくださいな

https://qiita.com/RyBB/items/0310ae89b76aef57da78

それでみなさん、良いお年を！ ≧(+･` ཀ･´)≦