Google Apps Script × スプレッドシートで作る「読書時間記録アプリ」
本記事では、
・フロントエンド(HTML / CSS / JavaScript)で入力操作
・バックエンド(GAS)でスプレッドシートへ保存
という流れを実装する。
1. 目標物の確認

操作イメージ:
- ブラウザでタイトルを入力して「読書開始」
- 終了時に「読書終了」を押す
- スプレッドシートにログが記録される
使用ツール
| 種類 | ツール | 用途 |
| データベース | Google スプレッドシート | 読書ログの保存 |
| サーバー処理 | Google Apps Script(GAS) | データ受信と書き込み |
| フロント側 | HTML/CSS/JavaScript | 入力フォームと通信 |
| 公開環境 | WordPressまたはcPanel | Webページ公開 |
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. 動作確認
- ブラウザで「index.html」を開く
- タイトルを入力→「読書開始」ボタンを押下
- 数秒後に「読書終了」ボタンを押下
- スプレッドシートにログが追加されていれば成功
よくあるエラーと対処
| 症状 | 原因 | 対応 |
| 「スクリプト関数が見つかりません:doget」 | URLを直接開いた | 想定通り。無視してOK |
| エラー率100% | doGet未定義 | 問題なし |
| 記録されない | Appsのアクセス権が「全員」になっていない | 設定を修正 |
| 自動終了が動作しない | トリガー未設定 | トリガーを再登録 |
7. サーバーへの配置
WordPressやサーバー環境(私の場合はcPanel)にアップロードする。
/public_html/lp/reading-tracker/
├── index.html
├── style.css
└── script.js
ブラウザでアクセス
8. まとめ
- スプレッドシートは空でもOK
- GASは「start/end」操作にも対応
- トリガーで未終了セッションも自動補完
この構成で、メンテナンス不要の軽量Webログアプリとして運用可能。
コメント
コメント一覧 (1件)
[…] 前記事にて紹介した手順を踏んで制作したのが当Webアプリ「Reading Tracker」になります。公開初期(前記事時点)と比べて、仕様も見た目もずいぶん変わりました。 […]