Google Apps Script × スプレッドシートで作る「読書時間記録アプリ」

Google Apps Script × スプレッドシートで作る「読書時間記録アプリ」

本記事では、
・フロントエンド(HTML / CSS / JavaScript)で入力操作
・バックエンド(GAS)でスプレッドシートへ保存
という流れを実装する。

1. 目標物の確認

操作イメージ:

  1. ブラウザでタイトルを入力して「読書開始」
  2. 終了時に「読書終了」を押す
  3. スプレッドシートにログが記録される

使用ツール

種類ツール用途
データベースGoogle スプレッドシート読書ログの保存
サーバー処理Google Apps Script(GAS)データ受信と書き込み
フロント側HTML/CSS/JavaScript入力フォームと通信
公開環境WordPressまたはcPanelWebページ公開

2. スプレッドシートを準備

Googleドライブにて新しいスプレッドシートを作成する。(空のままで問題なし)

※アプリ初回実行時に自動的に列(ヘッダー)と記録データが追記される。

3. Apps Script(GAS)の設定

0. GASとは

Google Apps Script(GAS)は、Googleが提供するクラウド上でプログラムを動かせる仕組み(スクリプト環境)。ブラウザだけでコードを書き、実行・公開まで行うことができる。
スクリプトはスプレッドシートと同じGoogleアカウントで管理でき、ブラウザだけで編集・実行・公開が可能。特別な環境構築やソフトのインストールは不要。
JavaScriptに似た文法で、GoogleスプレッドシートやGmailなどのGoogleサービスを自動化・拡張することができる。

今回の構成では、
フロントエンド(HTML/JavaScript)から送信したデータをGASが受け取り、スプレッドシートに記録する
という形で動作する。

GASはGoogleのサーバー上で実行されるため、自分でサーバーを用意する必要がない。
Webアプリとしてデプロイすることで、誰でもアクセス可能な簡易API(※)として利用できる。
これにより、HTML側からスプレッドシートにデータを送信できる。

API(Application Programming Interface)とは:あるサービスやアプリの機能を外部から呼び出せる”窓口”のこと。

1. スクリプトエディタを開く

スプレッドシート上部メニューから「拡張機能」→Apps Scriptを開く。

2. コード貼付

既存コードを削除し、以下をコピー&ペーストする。

function doPost(e) {
  try {
    const data = JSON.parse(e.postData.contents);
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
    const now = new Date();
    const action = data.action;
    const title = data.title;

    // シートが空ならヘッダーを自動作成
    if (sheet.getLastRow() === 0) {
      sheet.appendRow(["セッションID", "記録日時", "タイトル", "開始時刻", "終了時刻", "読書時間(分)", "状態"]);
    }

    if (action === "start") {
      const sessionId = Utilities.getUuid();
      sheet.appendRow([
        sessionId,
        Utilities.formatDate(now, "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss"),
        title,
        Utilities.formatDate(now, "Asia/Tokyo", "HH:mm:ss"),
        "",
        "",
        ""
      ]);
      return ContentService.createTextOutput(sessionId);
    }

    if (action === "end") {
      const sessionId = data.sessionId;
      const values = sheet.getDataRange().getValues();

      for (let i = values.length - 1; i >= 1; i--) {
        if (values[i][0] === sessionId && values[i][5] === "") {
          const startTime = new Date(`${Utilities.formatDate(now, "Asia/Tokyo", "yyyy/MM/dd")} ${values[i][3]}`);
          const endTime = now;
          const diffMinutes = Math.round((endTime - startTime) / 1000 / 60);
          sheet.getRange(i + 1, 5).setValue(Utilities.formatDate(endTime, "Asia/Tokyo", "HH:mm:ss"));
          sheet.getRange(i + 1, 6).setValue(diffMinutes);
          sheet.getRange(i + 1, 7).setValue("完了");
          return ContentService.createTextOutput("End logged");
        }
      }
      return ContentService.createTextOutput("No matching ID found");
    }

    return ContentService.createTextOutput("Invalid action");
  } catch (error) {
    return ContentService.createTextOutput("Error: " + error);
  }
}

function autoCloseSessions() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const values = sheet.getDataRange().getValues();
  const now = new Date();

  for (let i = 1; i < values.length; i++) {
    const status = values[i][6];
    const endTime = values[i][4];
    const startTimeStr = values[i][3];

    if (!endTime && (!status || status !== "完了")) {
      const startTime = new Date(`${Utilities.formatDate(now, "Asia/Tokyo", "yyyy/MM/dd")} ${startTimeStr}`);
      const diffHours = (now - startTime) / 1000 / 60 / 60;
      if (diffHours > 6) {
        const diffMinutes = Math.round((now - startTime) / 1000 / 60);
        sheet.getRange(i + 1, 5).setValue(Utilities.formatDate(now, "Asia/Tokyo", "HH:mm:ss"));
        sheet.getRange(i + 1, 6).setValue(diffMinutes);
        sheet.getRange(i + 1, 7).setValue("自動終了");
      }
    }
  }
}

3. デプロイ設定

右上「デプロイ」ボタン押下→「新しいデプロイ」押下

種類を「ウェブアプリ」に変更

設定内容:

項目
実行するユーザー自分(アカウント名)
アクセスできるユーザー全員(匿名含む)

4. 「デプロイ」→URLを控えておく

例:https://script.google.com/macros/s/AKfycbx_abc123/exec

※ブラウザに直接貼り付けるとエラー画面が出るが、問題ない。

4. 自動補完用トリガーの設定

Apps Script左側メニューの⏰アイコン

「トリガーを追加」→以下の内容で登録

項目設定内容
実行する関数autoCloseSessions
イベントの種類時間主導型
頻度毎時間

5. Webアプリの構築

ファイル構成

reading-tracker/
├── index.html
├── style.css
└── script.js

HTML・CSS・JSコード

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Reading Tracker</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>📚 Reading Tracker</h1>
    <p>タイトルを入力して読書時間を記録します。</p>
    <input type="text" id="title" placeholder="本のタイトルを入力">
    <div class="buttons">
      <button id="startBtn">読書開始</button>
      <button id="endBtn" disabled>読書終了</button>
    </div>
    <p id="status" class="status"></p>
  </div>
  <script src="script.js"></script>
</body>
</html>

style.css

body {
  font-family: "Segoe UI", "Noto Sans JP", sans-serif;
  background-color: #f9f9f9;
  color: #333;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.container {
  text-align: center;
  background: #fff;
  padding: 30px 40px;
  border-radius: 12px;
  box-shadow: 0 4px 10px rgba(0,0,0,0.1);
  width: 90%;
  max-width: 400px;
}

h1 {
  margin-bottom: 10px;
  color: #444;
}

p {
  color: #666;
  margin-top: 0;
  margin-bottom: 20px;
}

input[type="text"] {
  width: 90%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 6px;
  font-size: 16px;
  margin-bottom: 20px;
}

.buttons {
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
}

button {
  flex: 1;
  margin: 0 5px;
  padding: 10px 0;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  cursor: pointer;
  transition: 0.2s;
}

#startBtn {
  background-color: #4CAF50;
  color: white;
}

#startBtn:hover {
  background-color: #45A049;
}

#endBtn {
  background-color: #e74c3c;
  color: white;
}

#endBtn:disabled {
  background-color: #bbb;
  cursor: not-allowed;
}

.status {
  font-weight: bold;
  color: #444;
  min-height: 1.2em;
}

script.js

const gasUrl = "https://script.google.com/macros/s/AKfycbx_abc123/exec";
let sessionId = null;
let readingActive = false;

const startBtn = document.getElementById("startBtn");
const endBtn = document.getElementById("endBtn");
const status = document.getElementById("status");

startBtn.addEventListener("click", () => {
  const title = document.getElementById("title").value.trim();
  if (!title) return alert("タイトルを入力してください。");
  fetch(gasUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ action: "start", title })
  })
    .then(res => res.text())
    .then(id => {
      sessionId = id;
      readingActive = true;
      startBtn.disabled = true;
      endBtn.disabled = false;
      status.textContent = "読書中...";
    });
});

endBtn.addEventListener("click", () => {
  if (!sessionId) return;
  fetch(gasUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ action: "end", sessionId })
  })
    .then(() => {
      readingActive = false;
      startBtn.disabled = false;
      endBtn.disabled = true;
      status.textContent = "記録しました ✅";
      sessionId = null;
    });
});

window.addEventListener("beforeunload", (event) => {
  if (readingActive) {
    event.preventDefault();
    event.returnValue = "終了ボタンを押してから閉じてください。";
  }
});

6. 動作確認

  1. ブラウザで「index.html」を開く
  2. タイトルを入力→「読書開始」ボタンを押下
  3. 数秒後に「読書終了」ボタンを押下
  4. スプレッドシートにログが追加されていれば成功

よくあるエラーと対処

症状原因対応
「スクリプト関数が見つかりません:doget」URLを直接開いた想定通り。無視してOK
エラー率100%doGet未定義問題なし
記録されないAppsのアクセス権が「全員」になっていない設定を修正
自動終了が動作しないトリガー未設定トリガーを再登録

7. サーバーへの配置

WordPressやサーバー環境(私の場合はcPanel)にアップロードする。

/public_html/lp/reading-tracker/
├── index.html
├── style.css
└── script.js

ブラウザでアクセス

例:https://yourdomain.com/lp/reading-tracker/

8. まとめ

  • スプレッドシートは空でもOK
  • GASは「start/end」操作にも対応
  • トリガーで未終了セッションも自動補完

この構成で、メンテナンス不要の軽量Webログアプリとして運用可能。

この記事が気に入ったら
フォローしてね!

share
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする