Webアプリ「出退勤管理アプリ」を作ってみた

【Webアプリ】出勤管理アプリ⑨QRコードカードで出退勤打刻

自身の端末から出退勤打刻するページとは別に、QRコードカードを読み取り、打刻できるページを新たに作成します。

QRコードカード + iPhoneカメラ読み取り

構成例

  • カード:社員ごとのQRコードカード
  • 端末:iPhone(SafariまたはPWAアプリ)
  • 読み取り:iPhoneカメラ + JavaScript(WebRTC)でQRを読み取り
  • 記録:Firestoreなどに打刻時間・ユーザー情報を保存

メリット

  • iPhoneだけで完結可能
  • カード印刷も容易
  • Webアプリ上で完結しやすい

デメリット

  • 不正使用の可能性(QRコードコピー防止の工夫必要)

qr-attendance.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>QRコードで出退勤打刻</title>
    <script src="https://www.gstatic.com/firebasejs/10.11.0/firebase-app-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/10.11.0/firebase-firestore-compat.js"></script>
    <script src="https://unpkg.com/@zxing/library@latest"></script>
    <style>
        body {
            font-family: sans-serif;
            text-align: center;
            padding: 20px;
        }
        video {
            border: 1px solid #ccc;
            width: 300px;
            height: 200px;
        }
        button {
            margin: 10px;
            padding: 10px 20px;
            font-size: 16px;
        }
        #result {
            margin-top: 20px;
            font-size: 16px;
            color: green;
        }
        #clock {
            font-size: 18px;
            margin-bottom: 10px;
            color: #555;
        }
    </style>
</head>
<body>
    <h1>QRコードで出退勤打刻</h1>

    <div id="clock">--:--:--</div>

    <video id="preview" autoplay muted playsinline></video>

    <div>
        <button onclick="setType('出勤')">出勤モード</button>
        <button onclick="setType('退勤')">退勤モード</button>
    </div>

    <p id="result">モードを選択してください</p>

    <script>
        // Firebase 設定
        const firebaseConfig = {
            apiKey: "あなたのAPIキー",
            authDomain: "あなたのプロジェクトID.firebaseapp.com",
            projectId: "あなたのプロジェクトID",
            storageBucket: "あなたのプロジェクトID.appspot.com",
            messagingSenderId: "送信者ID",
            appId: "アプリID"
        };

        firebase.initializeApp(firebaseConfig);
        const db = firebase.firestore();

        let currentType = null;
        let codeReader = null;
        let cameraTimeout = null; // タイマー用変数

        // 時計表示
        function updateClock() {
            const now = new Date();
            const timeStr = now.toLocaleString('ja-JP', { hour12: false });
            document.getElementById("clock").textContent = timeStr;
        }
        setInterval(updateClock, 1000);
        updateClock();

        function setType(type) {
            // 前回のカメラを停止
            if (codeReader) {
                codeReader.reset();
            }
            if (cameraTimeout) {
                clearTimeout(cameraTimeout);
            }

            currentType = type;
            document.getElementById("result").innerText = `${type}モードです。QRコードをかざしてください。`;

            startCamera();
        }

        function startCamera() {
            const previewElement = document.getElementById('preview');
            codeReader = new ZXing.BrowserQRCodeReader();

            // 1分後に自動停止
            cameraTimeout = setTimeout(() => {
                codeReader.reset();
                document.getElementById("result").innerText = "カメラは1分後に自動停止しました。再度モードを選択してください。";
            }, 60000);

            codeReader.decodeOnceFromVideoDevice(undefined, previewElement)
                .then(async (result) => {
                    clearTimeout(cameraTimeout);
                    const uid = result.text;
                    const nowDate = new Date();

                    let latitude = null;
                    let longitude = null;
                    let address = "住所不明";

                    if (navigator.geolocation) {
                        navigator.geolocation.getCurrentPosition(async (pos) => {
                            latitude = pos.coords.latitude;
                            longitude = pos.coords.longitude;

                            try {
                                const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=ja`);
                                const data = await res.json();
                                address = data.display_name || address;
                            } catch (e) {
                                console.warn("住所取得に失敗:", e);
                            }

                            saveAttendance(uid, nowDate, latitude, longitude, address);
                        }, (err) => {
                            console.warn("位置情報取得失敗:", err);
                            saveAttendance(uid, nowDate, latitude, longitude, address);
                        });
                    } else {
                        saveAttendance(uid, nowDate, latitude, longitude, address);
                    }

                    document.getElementById("result").innerText = `${uid} の ${currentType} を記録中...`;
                    codeReader.reset();
                })
                .catch((err) => {
                    clearTimeout(cameraTimeout);
                    console.error("QRコードの読み取りに失敗:", err);
                    document.getElementById("result").innerText = "QRコードの読み取りに失敗しました。もう一度試してください。";
                });
        }

        function saveAttendance(uid, now, latitude, longitude, address) {
            db.collection("attendance").add({
                uid: uid,
                type: currentType,
                timestamp: firebase.firestore.Timestamp.fromDate(now),
                location: {
                    latitude,
                    longitude,
                    address
                }
            })
            .then(() => {
                document.getElementById("result").innerText = `${uid} の ${currentType} を記録しました(${now.toLocaleString()})`;
            })
            .catch((error) => {
                document.getElementById("result").innerText = `記録に失敗: ${error.message}`;
            });
        }
    </script>
</body>
</html>

コード解説

このQRコードで出退勤打刻を行うWebアプリのコードは、以下のような構成と仕組みになっています。

✅ 全体の目的

出勤 or 退勤ボタンを押すとカメラが起動し、QRコードを読み取って、位置情報付きで出退勤をFirebaseに記録する」という流れです。

🔧 構成と解説

🔹 1. HTML構造

<video id="preview" autoplay muted playsinline></video>
  • カメラ映像を表示するビデオ要素。
<button onclick="setType('出勤')">出勤モード</button>
<button onclick="setType('退勤')">退勤モード</button>
  • 出勤 or 退勤モードを選択するボタン。押すとカメラが起動します。
<div id="clock">--:--:--</div>
  • 常に更新される現在時刻。
<p id="result"></p>
  • 処理結果やエラーメッセージを表示します。

🔹 2. Firebase 初期化

const firebaseConfig = {
  // Firebaseプロジェクトの設定
};

firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
  • Firestore データベースにアクセスするための初期設定。

🔹 3. 現在時刻を常時表示

function updateClock() {
    const now = new Date();
    const timeStr = now.toLocaleString('ja-JP', { hour12: false });
    document.getElementById("clock").textContent = timeStr;
}
setInterval(updateClock, 1000);
  • 毎秒現在時刻を更新して画面に表示しています。

🔹 4. モード切替とカメラ起動

function setType(type) {
    currentType = type;
    document.getElementById("result").innerText = `${type}モードです。QRコードをかざしてください。`;
    startCamera();
}
  • 出勤 or 退勤 のどちらかのモードを選び、カメラを起動します。

🔹 5. カメラ起動とQRコード読み取り

function startCamera() {
    const previewElement = document.getElementById('preview');
    codeReader = new ZXing.BrowserQRCodeReader();

    codeReader.decodeOnceFromVideoDevice(undefined, previewElement)
  • decodeOnceFromVideoDevice() は、1回だけQRコードを読み取ってカメラを自動停止してくれる便利な関数です。
.then(async (result) => {
    const uid = result.text;
    const nowDate = new Date();
  • QRコードから取得した文字列(社員のuidなど)を変数uidに格納。
  • nowDate は現在の日時。

🔹 6. 位置情報の取得

if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(async (pos) => {
        latitude = pos.coords.latitude;
        longitude = pos.coords.longitude;
  • ブラウザから現在の位置情報(緯度・経度)を取得します。
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?...`);
  • オープン地図APIを使って住所を取得します。

🔹 7. 出退勤の保存処理

function saveAttendance(uid, now, latitude, longitude, address) {
    db.collection("attendance").add({
        uid: uid,
        type: currentType,
        timestamp: firebase.firestore.Timestamp.fromDate(now),
        location: {
            latitude,
            longitude,
            address
        }
    })
  • attendance コレクションに次の情報を保存:
    • uid: 誰が
    • type: 出勤 or 退勤
    • timestamp: いつ
    • location: 緯度・経度・住所

🔹 8. QR読み取り後のカメラ停止

codeReader.reset();
  • QRコード読み取りが成功したら、カメラを停止します。
  • 電力節約や誤動作防止のため。

✅ 主な技術とライブラリ

技術説明
Firebase Firestore出退勤データの保存
ZXing (@zxing/library)QRコードの読み取り
Geolocation API現在の緯度・経度を取得
OpenStreetMap (Nominatim API)緯度経度 → 住所への変換
JavaScript全体の制御、DOM操作、非同期処理

✅ 例:Firestoreに保存されるデータ

{
  "uid": "abc123",
  "type": "出勤",
  "timestamp": "2025-05-19T22:10:00+09:00",
  "location": {
    "latitude": 35.6895,
    "longitude": 139.6917,
    "address": "東京都新宿区..."
  }
}