2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

屋外イベントで使えるスタンプカードシステムを静的HTMLで作った

Posted at

はじめに

とあるイベントの運営に関わらせてもらって、スタンプカードがほしいなー となりました。チェックポイントでQRコードを読んだら、スタンプが押されていく仕組み。
image.png
こんなのが、QRを読んだ後に出てくればよし。(↑は最終的にできたシステム)

動作の確認はこちら。

ウェブサービスを調べると、有料だったり、過剰だったり、ログインとかユーザー登録とかさせたくないし。そんなヘビーなものを求めてないんですよね。

考えてみたら、静的HTMLでも簡単にできる んじゃね?ということで、やってみたらできましたの話。欲する人は多いけど、エンジニア的には簡単、というおいしい話です。

ソースはこちらにあります。

要件の整理

  • スタンプは、特定のURLへアクセスする(QRコードを読み取る)ことで押すことになる
  • スタンプの設置台ごとのURLは、card.html?stampId=stamp1など
  • 設置台ごとにIDが異なり、押されるスタンプの絵も異なる
  • ユーザーは、自分でスタンプの状態を確認できる
  • イベントは1日だけで、小規模なものなので、ズルをしてたくさんスタンプを押す対策などは、あんまり考えなくてよい

方法概要

  • クッキーを使って、ユーザー(のスマホ)を識別する
  • 同様にクッキーを使って、スタンプを押した状態を保持する
  • URLパラメータからスタンプ台を認識して、どのスタンプを押したかを識別する

いや、大したことなさすぎて震えます。

詳細

私はReactでやったのでReactで説明しますが、素のJavaScriptでもできちゃう、むしろその方がいいまであるレベルです。

本当に細かいところは、githubをご覧ください。

state

ユーザーIDとスタンプID配列をuseStateで定義します。初期値は、クッキーから取得。初回アクセス時は、ユーザーIDを生成したいので、getOrCreateです。

StampCard.tsxの一部
const [userId, setUserId] = useState<string>(
    getOrCreateUniqueUserId()
);
const [stampIds, setStampIds] = useState<string[]>(
    getCookieValues("stamp_ids")
);

一応getOrCreateUniqueUserId()getCookieValues()も載せておきます。

cookieUtils.tsの一部
// クッキーを設定する
export const setCookie = (key:string, value:string) => {
    document.cookie = `${key}=${value}; ` +
      `path=/; max-age=${COOKIE_EXPIRATION_TIME}`;
}

// クッキーからユーザーIDを取得する or 作成する
export const getOrCreateUniqueUserId = ():string => {
    const key = 'user_id';
    const match = document.cookie.match(new RegExp(`${key}=([^;]+)`));
    if (match) return match[1];
    const id = crypto.randomUUID();

    // 1日だけ有効なクッキーを作成する
    setCookie(key, id);

    return id;
}

// クッキーから文字列の配列を取得する
export const getCookieValues = (key: string):string[] => {
    const match = document.cookie.match(new RegExp(`${key}=([^;]+)`));
    if (match) return match[1].split(',');
    return [];
}

新しいスタンプが押された処理

URLパラメータとして、/?stampId=stamp1などとstampIdが渡ってくるので、これをuseEffect()で受け取って、stateのstampIdsを更新します。

StampCard.txtの一部
// 新しいスタンプを、stampIdsに追加し、クッキーを更新する
const addNewStamp = (newStampId:string) => {
    // 今回のスタンプIDが、存在していなければ追加する
    if (!stampIds.includes(newStampId)) {
        const newStampIds = [...stampIds, newStampId];

        // state値を更新
        setStampIds(newStampIds);

        // クッキーを更新
        setCookie('stamp_ids', newStampIds.join(','));
    }
}

useEffect(() => {
    // URLパラメータからstampIdを取得
    const params: URLSearchParams = new URLSearchParams(
        window.location.search
    );
    const newStampId: string | null = params.get("stampId");
    
    if (newStampId) {
        // スタンプIDが指定されている場合、stampIdsに追加し、クッキーを更新する
        addNewStamp(newStampId);
    }
}, []);

Cookieには、user_idとして"6477f618-be29-4e59-a7d8-d29afdda271e"のようなUUID文字列が、stamp_idsとして"stamp1,stamp2"のようなカンマ区切りのスタンプID文字列が入ります。

image.png
(dev tool>アプリケーション>Cookieの画面)

描画

あとはStampID配列を描画するだけです。

スタンプカードに合わせて、スタンプIDごとに位置を定義しておいて、それをスタンプカードの画像の上に重ねて置くだけ。

完成形のhtmlイメージ
<div style="position: relative;">
  <img src="stamp_card.png" />
  <img src="stamp1.png"
    style="
      position: absolute;
      left: 291;
      top: 173;"
  />
</div>

これをReactで書くだけなので、TypeScriptのコードは割愛。

基本的にはここまでです。

Next Step

機能を追加していきます。

1: 初期化

野外のイベントをやってたら何があるかわからないので、まっさらにしてやり直ししたいことがあるでしょうということで、URLパラメータとしてinitializeが渡ってきたら、全部消すという処理を追加しました。

StampCard.tsxの一部
const [userId, setUserId] = useState<string>(getOrCreateUniqueUserId());
const [stampIds, setStampIds] = useState<string[]>(
    getCookieValues("stamp_ids")
);

useEffect(() => {
    // ・・・省略・・・

    // クッキーの削除パラメータがあった場合は、削除する
    const initializeParam: string | null = params.get("initialize");
    if (initializeParam) {
        deleteCookie('user_id');
        deleteCookie('stamp_ids');
        setUserId(getOrCreateUniqueUserId());
        setStampIds([]);
    }
}
cookieUtils.tsの一部
// クッキーを削除する
export const deleteCookie = (key:string) => {
    document.cookie = `${key}=; path=/; max-age=0`;
}

2: Cookieの利用拒否

ブラウザの設定で、Cookieの利用を拒否できるので、それを検知したいと思いました。変なセキュリティ意識の高い人が拒否ってそう。イベント中にQRコードを読み込んで「何も起きないんですけどー!」とか発生しうる。

意識高い系の人のためになんて対応はめんどくさいですが、現場でトラブルが起きるよりは、事前にやれることとしてやっておきます。

クッキーを書いて、すぐ読むことができるかどうか、というチェックです。

cookieUtils.tsの一部
// Cookieが有効かどうかを確認する
export const checkCookieEnabled = (): boolean => {
    const testKey = 'cookie_test';
    document.cookie = `${testKey}=1; max-age=10; path=/`;
  
    const match = document.cookie.match(new RegExp(`${testKey}=([^;]+)`));
    const result = !!match;
  
    // テスト用Cookieを削除(副作用防止)
    document.cookie = `${testKey}=; max-age=0; path=/`;
  
    return result;
};

それをstateに設定して、

StampCard.tsxの一部
const [cookieEnabled] = useState<boolean>(checkCookieEnabled());

描画する。

StampCard.tsxの一部
<div>
    <p>ブラウザ設定で、Cookieが有効かどうか</p>
    <p>{cookieEnabled ? '有効' : '無効'}</p>
</div>

「クッキーが無効なので使えませーん!」というメッセージを出す感じです。

3: 利用ログ収集

ちょっと別の視点で、スタンプが押されたら、Google APIを使ってBigQueryへデータを蓄積したいと思いました。このデータはクライアントでは使用せず、後日、分析に使う。行動の時系列データなので分析しがいがありそうです。売り上げと重ねたら面白い。

方法は、静的HTMLでも、どこかへPOSTするだけならできます。GASへPOSTして、GASがBQに入れればいいです。レコード件数が少なければ、Spread Sheetでもいい。とにかくここでは、POSTすることの技術的な確認をします。

useEffect()から、次の関数を呼びます。

postStamp.ts
// スタンプ情報をPOSTするURL
const POST_URL = "http://hoge.com/api/stamp";

// スタンプが追加された旨をPOSTする(非同期で実行し、クライアント側へは特に通知しない)
export const postStamp = (userId:string, stampId:string) => {
    fetch(POST_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, stampId }),
    })
    .then((res) => {
        if (!res.ok) throw new Error('通信エラー');
        return res.json();
    })
    .then(() => {
        console.log(`スタンプ「${stampId}」を獲得しました!`);
    })
    .catch(() => {
        console.log('通信に失敗しました。もう一度お試しください。');
    });
}

console.log()は、出しても出さなくてもいいけど。

ところで、Cookieの使用許諾?

最近よく「Cookieを使っていいか?」っていうメッセージ(同意バナーと呼ぶ)が出ますので、あれの説明。

まず、ブラウザの設定でCookieの使用可否。これが唯一の技術的な壁です。ここで拒否されていたら使えないし、許可されていたら使える。

では「Cookieを使っていいか?」とは何か? その質問に 「ダメ」と答えられたら、自主的に見ない というだけです。そこでどう答えようと、技術的には見られる。

ではなぜそれを聞くかというと、個人情報の扱いの問題。Cookieは個人情報であると解釈されることもあるので(特にヨーロッパ圏、GDPR対応というキーワードで調べて)、法的リスクの観点で危ない橋を渡らない ように、事前に確認して、OKなら使うしダメなら使わないということです。

日本では、IPアドレスは個人情報 です。これは確定。でも それ以外は微妙 で、特に単独では、どちらかというと個人情報ではないものが多い。(←判断は自己責任でお願いします。)なので、今回のスタンプカードのクッキーを保存して読むのはセーフと、私は考えました。

ということで、私が使おうとしているのは小さなイベントでもあるし、同意バナーは出さず、勝手にUUIDとStampIDを保存して読むことにします。

まとめ

技術的に簡単という話だったので、一瞬で終わりそうでしたが、改造の話を書いていたら長くなって疲れました。

スタンプカードの利用は、イベントではよくありそうだし、簡単に使えるのであれば使いたいですよね。私は、イベントのウェブサイトの1ページを使おうと思っています。

今ふと思いついたんですが、ウェブサイトの一部でもなく、本当にスタンプカードだけのシステムが欲しいなら、GASでhttpサーバー立てればできますね。コピペも簡単。忘れないうちに作っておこうかな🤔

ではよきスタンプライフを!

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?