ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

日々のdaily scrumでアイスブレイクの話題ガチャつくって運用してみた

こんにちは。サーバーサイドエンジニアの伊東です。

この記事はユニファAdvent Calendar 2023の20日目の記事です。

adventar.org

私達のチームでは、daily scrumの冒頭にアイスブレイクを行っています。

アイスブレイクは、テーマに沿った話を1〜3分程度簡単に話すことで行っています。 テーマは、メンバー全員が1周するまで固定で、話す人は毎日入れ替えて実施しています。 そんなアイスブレイクですが、テーマの選定に悩むこともありました。

今回、技術の習得も兼ねて、簡単な話題を引けるガチャを作ってみることにしました。(趣味で)

つくってみた話題ガチャ

雑談話題のガチャは下記のような見た目になります。

雑談話題ガチャスクショ

「話題を引く」をクリックしてランダムで話題が表示されるシンプルなアプリです。 話題やアイコンはChatGPTと相談しながら作りました。

雑談話題のガチャの技術スタック

今回は、Cloudflareについて勉強したかったので、Cloudflareの各種サービスを使って作成しました。下記を使っています。

  • Cloudflare Workers・・・AWS Lambdaのようにサーバーレスで動作するファンクション(jsで書く)。ゼロコールドスタート。
  • Cloudflare Pages・・・静的なページをホスティングできます。
  • Cloudflare D1(まだBeta)・・・サーバーレスのSQL DB。Sqliteがベースとのこと。

Cloudflare Workers

このアプリのバックエンドになります。

  • Workersでは、Honoというweb frameworkを使っています。 hono.dev

  • D1へのアクセスは、workers-qbというライブラリを利用しました。 github.com

下記2つのAPIを用意しました。

  • /api/topics ・・・話題の全取得。
  • /api/topics/random・・・ランダムで1件の話題を取得。

※今回のユースケースでは、全部取得した中から、フロントエンドで1件ランダムで話題を選べば事足りるのですが、勉強のためランダムに取得するapiをはやしてみました。

下記2つのファイルで構成されています。

topics/api.ts

import { Hono } from 'hono'
import { D1QB, FetchTypes, OrderTypes } from "workers-qb";

export interface Env {
  DB: D1Database;
}

const topics = new Hono<Env>();

// 全話題を取得
topics.get('/', async c => {
  console.log('c', c);

  const qb = new D1QB(c.env.DB);
  const fetched = await qb
  .fetchAll({
    tableName: 'topics',
    fields: ['*'],
    orderBy: {
      created_at: OrderTypes.DESC,
    },
  })
  .execute()

  console.log('fetched', fetched);
  return c.json(fetched.results);
})

// ランダムで1件の話題を取得
topics.get('/random', async c => {
  try {
    const qb = new D1QB(c.env.DB);
    const fetched = await qb
    .fetchAll({
      tableName: 'topics',
      fields: '*',
      orderBy: 'RANDOM()',
      limit: 1
    }).execute();
    console.log(fetched)
    return c.json(fetched.results);
  } catch (e) {
    console.error(e);
    throw e;
  }
});

index.ts(主にCORSの設定をしています)

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { topics } from './topics/api'

const app = new Hono()
app.use(
  '/api/*',
  cors({
    origin: ['http://localhost:3001'],
    allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests', 'Content-Type'],
    allowMethods: ['POST', 'GET', 'OPTIONS'],
    exposeHeaders: ['Content-Length', 'X-Kuma-Revision'],
    maxAge: 600,
    credentials: true,
  })
)

app.route('/api/topics', topics)

export default app

Cloudflare Pages

このアプリのフロントエンドになります。

  • React、Tailwindcssなどを利用しました。

先程のCloudflare WorkersのバックエンドAPIを叩きます。 「話題を引く」ボタンのコンポーネントだけ下記に貼ります。

import React, { useState } from "react";

export default function RandomTopicButton(){
  const [text, setText] = useState(""); // 新しいstateを追加
  const [isLoading, setIsLoading] = useState(false); // ローディング状態のstate
  const [animate, setAnimate] = useState(false); // アニメーション状態を追加

  const handleClick = async () => {
    setIsLoading(true);
    setAnimate(false); // アニメーション状態をリセット
    try{
      const apiUrl = process.env.REACT_APP_CF_BACKEND_API_URL;
      const url = `${apiUrl}/api/topics/random`;
      const response = await fetch(url);
      const data = await response.json();
      setText(data[0].body);
      setAnimate(true); // テキストがセットされたらアニメーション状態をtrueに
    } catch (error) {
      if (error instanceof Error) {
        alert('エラーが発生しました: ' + error.message);
      } else {
        // errorがErrorインスタンスではない場合の処理
        alert('予期せぬエラーが発生しました');
      }
    }
    setIsLoading(false);
  }

  // 使用する絵文字のリスト
  const emojis = ["👻", "😃", "👐", "👀", "🙌", "🐶", "🐹", "🤣", "⭐️", "⚽️"];
  const getRandomEmoji = () => emojis[Math.floor(Math.random() * emojis.length)];


  return (
    <div className="container py-16 flex justify-center flex-col">
      {text &&
        <div style={{ wordBreak: ('auto-phrase' as any) }} className={`w-full px-4 my-10 py-10 ${animate ? 'animate-scale-in-center' : ''}`}>
          <div className="text-center font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600 text-9xl">{text}</div>
        </div>
      }

      <div className="flex justify-center">
        <button type="button"
                disabled={isLoading}
                onClick={handleClick}
                className=" text-4xl py-2.5 px-32 me-2 mb-2 font-medium text-gray-900 focus:outline-none bg-white rounded-full border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200">
          {isLoading ? '読み込み中...' : `話題を引く ${getRandomEmoji()}`}
        </button>
      </div>
    </div>
  )
}

ちょっと工夫したことは、最近chrome等で利用できるスタイルの「word-break: 'auto-phrase'」を利用したことです。 これは、日本語の分節で自動改行してくれるものです。引いた話題のテキストがいい感じに表示できて嬉しいです。

iwb.jp

すべてのコードが気になる方は下記リポジトリを参照してください。 github.com

実際に雑談ガチャを運用してみてどうだったか

これを使ってメンバーで一周してみた後のフィードバックは下記です。

  • ガチャを引く面白さと、その場ですぐに話を考えるアドリブ感が面白かった。
  • テーマは有限であるため、ガチャにフリーテーマを入れてみるのも良いかもしれない。
  • ガチャを引かなくても1日1回話題が選出されている状態になっていても良いかも。

デイリースクラムはいつもやっているとマンネリ化しがちですが、少しばかりのスパイスを加えることが出来た気がします。 今後頂いたフィードバックをもとに改良していこうと思いました。

まとめ

CloudflareのPagesやWorkersを使って簡単なwebアプリがすぐに作る事ができました。 趣味程度で作成して公開するなら費用ゼロで可能でした。 Cloudflareのworkersなどは、かなり実用的な使い道があるので使い方を模索したいです。

今回、アイスブレイクの話題選びとして簡単なWebアプリを作ってみましたが、とても良い勉強になりました。

==

最後までお読みいただきありがとうございました!

ユニファでは、一緒にはたらく仲間を募集しています!

unifa-e.com