この記事は生成AIを使って出力されたものを編集したものです。
はじめに
社内のプロジェクト管理用(プロジェクト → グループ → タスクの階層で進捗を管理する)に、タスク管理&ガントチャートWebアプリを Claude Code と二人三脚(実質「1人+AI」のアジャイル開発)で作った。
- フロント:HTML + Tailwind CSS + 素のJavaScript
- バック/DB(本番):Google Apps Script + Googleスプレッドシート
- 開発体制:ドメイン知識を持つ自分1人 + Claude Code
「まずブラウザだけで動くローカル試作版を作り、画面と業務ルールを固めてから GAS へ移植する」という進め方を取った結果、試作段階では気づけず、GAS移植で初めて表面化した罠がいくつもあった。この記事では、要件定義書・仕様書・設計書という3種のドキュメントをAIとの共同作業のハブにしたやり方と、そこで得た技術的な知見を共有する。
対象読者:Claude Code(またはAIコーディングエージェント)を使って個人・小規模チームで業務アプリを作ろうとしている人、GAS+スプレッドシートをバックエンドにしたWebアプリを検討している人。
作ったもの
プロジェクト1
├ タスク(プロジェクト直下)
├ グループ1
│ ├ タスク1
│ └ タスク2
└ グループ2
├ タスク1
└ タスク2
この階層をツリー表示する「リストビュー」と、タイムライン表示する「ガントビュー」の2画面構成。上位階層(プロジェクト・グループ)の進捗・工数・日付は下位から**自動集計(ロールアップ)**され、直接編集はできない。
ドキュメントを3層に分けてAIとの土台にする
個人開発でありがちな失敗は、「なぜ作るか」「何を作るか」「どう作るか」が頭の中でごちゃまぜのままAIに指示を出し、セッションが変わるたびに前提の説明からやり直すことだ。そこで最初に、役割の異なる3つのMarkdownを用意した。
| ドキュメント | 役割 | 更新頻度 |
|---|---|---|
requirements.md |
なぜ作るか(背景・目的・スコープ) | ほぼ固定(着想時点のまま保存) |
specification.md |
何をするか(機能・画面・業務ルール・論理データ項目) | 仕様変更のたびに更新 |
design.md |
どう実現するか(物理データモデル・ER図・技術構成) | 実装判断のたびに更新 |
さらに design.md は「本編=現在の正」と「付録:意思決定の経緯」を分離した。決定した内容そのものと、そこに至った検討過程(不採用にした案とその理由)を同じ文書内で分けて残すことで、本編は常に最新の正として簡潔に保ちながら、「なぜ今の形になったか」を後から追える。AIエージェントは長期記憶を持たないので、**この経緯ログが実質的な"引き継ぎメモ"**として機能する。
加えて ~/app/CLAUDE.md を専用の開発引き継ぎファイルとして育てた。ここには技術スタック、ファイル構成、「よく変更する場所」の早見表、そして決定事項を通し番号で40件以上蓄積している。新しいセッションを始めるとき、この1ファイルを読ませるだけでAIが前回までの文脈を復元できる。
学び:AIとの協働では「今何を作っているか」より「なぜその形にしたか」を記録する方が効く。コードは読めば分かるが、不採用にした設計判断はコードに残らないため、明示的にログしないと同じ議論を繰り返すことになる。
試作 → GAS移植という進め方にした理由
Google Apps Script は無料で使え要件にも合っていたが、いきなりGAS上で開発すると以下の理由で反復が遅くなる。
- コード変更のたびに
clasp push→ デプロイが必要で、ブラウザのCtrl+Sでリロードでは確認できない - GASの実行環境(V8ランタイムだが
HtmlServiceはサンドボックス化されたiframe内で動く)特有の制約が、業務ロジックの検証を邪魔する
そこで 「業務ロジック・画面・操作感」は素のHTML/JS試作版で先に固め、確定してからGASへ移植する方針を取った。この判断は結果的に正しかったが、「試作版で問題なかったものが、GAS移植で初めて壊れる」という現象が3つ発生した。これがこの記事で一番共有したい知見である。
罠1:iframeサンドボックスがネイティブAPIをブロックする
GASの HtmlService で配信される画面は、クロスオリジンのiframe内に表示される。試作版(同一オリジンで python -m http.server 配信)では問題なく動いていた以下の実装が、本番GASでだけ壊れた。
日付入力:当初、隠しの <input type="date"> を配置し showPicker() でネイティブカレンダーを開く方式を採っていた。iframeサンドボックス内では showPicker() がブロックされ、GAS上でだけカレンダーが開かない。しかも失敗時に編集状態が残ってしまい、一度📅を押すと他のセルが編集不能になる副作用まで出た。
対策として、ネイティブAPIに依存しない自前の月グリッドカレンダー(openCalendarPopup)を core.js に実装し置き換えた。テキスト欄への YYYY-MM-DD 手入力・日付の実在チェック(normDate)・前後関係チェック(dateOrderError)といった呼び出し側の契約はそのままに、内部実装だけ差し替えることで影響範囲を1関数に閉じ込められた。
プロジェクト絞り込みの画面間同期:リストビューとガントビューで「どのプロジェクトを表示するか」を同期する仕組みを、当初は localStorage + storage イベントで実装していた。GASのiframe環境では親URLの直接参照やstorageイベントの挙動が試作版と異なり、期待どおりに同期しない。URLパラメータ(?projects=ID,ID)方式に置き換え、初期値はサーバ側の doGet(e.parameter.projects) が画面に注入し、以後の変更は google.script.history.replace で反映する形にした。副次効果として、ブックマークや共有URLでも同じ絞り込み状態を再現できるようになった。
学び:GAS + HtmlService を使う場合、ブラウザネイティブAPI(showPicker、localStorage同期、window.open系)はサンドボックス越しに動くかどうかを個別に確認する必要がある。試作段階で「同一オリジン限定の代替実装」を用意しておくと移行コストを抑えられる。
罠2:ブラウザ間でのイベント発火順序の違い
Mac の Safari / Chrome では <select> を開くとOS標準メニューにフォーカスが移り、blur イベントが選択確定より先に発火する。Windows Chrome はドロップダウンを自前描画するため blur が出ずこの問題が起きない。
インライン編集の確定処理を blur イベントに統一していたため、Mac環境では「未選択のまま blur で確定 → 直後の change は既に確定済み扱いで無視される」というバグが起き、1行目のステータスを変更すると2行目以降が変更できなくなるという致命的な症状につながった。
対策は <select> だけ確定タイミングを change に、それ以外(テキスト・数値)は従来どおり blur にする、という要素種別ごとの分岐。OS/ブラウザ差はローカルのWindows環境での動作確認だけでは絶対に踏み抜けないため、実運用者の環境(Mac)でのフィードバックが無ければ長期間気づかなかった可能性が高い。
学び:フォーム系のブラウザイベントは「どのOS・ブラウザで検証したか」を明示しておく。個人開発では自分の開発機以外の環境で動作確認する機会が乏しいので、要素の種類(テキスト/セレクト/日付)ごとに確定イベントを設計段階で使い分けておくとリスクを減らせる。
罠3:alert / confirm はGAS本番で使わない
試作段階から一貫して alert / confirm / ネイティブ prompt を避け、独自モーダル(openMessageModal / openConfirmModal / openInputModal)に統一していた。これはUXの見た目を揃える意図で始めたが、後から振り返るとGAS移植の観点でも正解だった。ネイティブダイアログはiframe内での挙動がブラウザ・環境依存になりやすく、サンドボックス制約の影響を受けやすい領域だからだ。
学び:GAS(iframe配信)を将来のデプロイ先として想定するなら、ネイティブブラウザダイアログには早い段階で見切りをつけ、自前モーダルに統一しておくと移植時の手戻りを避けられる。
データモデルの正規化:作りながら育てる設計
最初の素案(requirements.md に残る「着想時点の案」)は、プロジェクト・グループの行に「担当者」を直接カラムとして持つ非正規形だった。
プロジェクトTBL: プロジェクトID, 会社名, プロジェクト名, プロジェクト担当者, ...
グループTBL: グループID, 会社名, グループ名, カテゴリ, グループ担当者, ...
開発を進める中で「担当者は複数人になりうる」「顧客側の担当者と自社の担当者は属性が違う(顧客担当者だけ連絡先や役割を持つ)」「タスク名が表記ゆれする」といった要件が明らかになり、以下の正規化を段階的に実施した。
-
担当割当の中間テーブル化:プロジェクト・グループ・タスクの行に担当者IDの配列を直接持たせる案(複製案)はPK設計が壊れるため不採用にし、
assignments(割当ID, 対象種別, 対象ID, メンバーID, 役割)という中間テーブルに切り出した。 -
タスク名の作業マスター化:タスクは独自の名前を持たず、
works(作業ID, 作業名, 標準工数)マスターを参照して名称を表示する方式に変更。表記ゆれを防ぎつつ、標準工数をテンプレートとして使えるようにした。 -
担当者と顧客担当者の分離:自社メンバー(
members)と顧客側の担当者(customers)は持つ属性が違う(顧客側だけ連絡先・役割を持つ)ため、別マスターに分離。assignments.メンバーIDには自社=M接頭辞/顧客=CU接頭辞のIDを格納し、役割カラムと整合させた。
このリファクタは「最初から正規化されたスキーマを設計する」よりも、動くものを触りながら業務要件のズレに気づいて段階的に正規化するやり方のほうがAIとの協働では現実的だった。試作版はスプレッドシートではなくJS配列(data.js)だったため、スキーマ変更のコストが低く何度もリファクタできたのも大きい。「本番DB(スプレッドシート)で直接試行錯誤しない」ことの価値を実感した。
2画面共有ロジックの切り出し(core.js)
リストビューとガントビューは別々のHTML/JSファイルだが、集計ロジック・保存管理・マスター参照・バリデーションなど「業務ロジック」は完全に共通である。当初はこれらが app.js(リスト用)と gant.js(ガント用)に重複していたが、core.js に一本化し、画面固有の違いは「フック」で吸収する設計にした。
読み込み順(両画面共通):data.js → core.js → app.js(または gant.js)
-
refreshView():破棄(変更取消)後の再描画。リストはrender、ガントはrenderAllを実装 -
refreshMemberWidgets():メンバー変更時のUI再構築。リストのみ実装(ガントは未定義でも無害)
集計ルール(加重平均・ロールアップ)を1箇所に集約したことで、後述する「進捗計算バグ」の修正も1行の変更で両画面に反映できた。フックで差分を吸収する設計は、"2画面同期のバグ"というカテゴリのバグをそもそも発生させないという効果がある。
保存はオプトイン方式に統一する
編集・追加・削除・貼り付け・マスターの新規登録は、すべて画面には即反映するが「未保存」として扱い、ツールバーの「保存(更新)」を押すまでスプレッドシートには書き込まない。「変更を破棄」で最後に保存した状態に戻せる。
-
takeSnapshot/restoreSnapshot:保存時点の全データ(マスター含む)を丸ごとスナップショット -
markDirty:変更検知 -
doSave/doDiscard:確定 / 破棄
これにより「間違えて消したらどうしよう」という不安なく画面上で試行錯誤でき、業務ユーザーへのヒアリングでも好評だった。実装コストは掛かるが、スプレッドシートを直接叩くタイプの業務アプリでは「元に戻せる」という安心感がUXの核になる。
副次的な設計判断として、プロジェクト・グループの削除確認は当初「名称の入力一致を必須にする」方式を検討したが、「オプトイン保存である以上、確認モーダルだけで十分(保存しなければ反映されない)」と判断して簡素化した。安全策を機械的に積み増すのではなく、すでにある仕組み(オプトイン保存)が担保している安全性を踏まえて、UI側の防御レベルを決めるという考え方は他の場面にも応用できる。
選択・一括操作UIの反復設計
プロジェクト・グループ・タスクを一括選択して「タスクを生成」「値を一括変更」「削除」を行うUIは、要件が明確になるまで3回作り直した。
- 初期案:チェックした行を親子関係を問わず個別に選択
- 改善案:上位(プロジェクト・グループ)のチェック状態を配下タスクの選択状況から自動反映(
indeterminate状態を活用)。「配下が一部選択=−」「自身のみ選択=✔」を明確に区別 - 最終案:ヘッダーのマスターチェックボックスを「✓ で表示中の全タスクだけを選択」に変更し、以前必要だった「タスクだけに絞り込む」ボタン操作を1ステップ省略
最初から完璧な仕様を決め打ちせず、実際に触って「思っていたのと違う」を都度潰していくやり方は、AIとの協働だと反復コストが低いため相性が良い。ただし「チェックは下位へ連動しない(誤操作防止のため上位チェックはあくまで表示状態)」のような細かい仕様は、口頭説明だけでは実装ごとにブレやすいので、決定した瞬間に design.md の決定ログへ番号付きで記録し、以後は参照するようにした。
まとめ
Claude Codeと1人アジャイルで業務アプリを開発する中で得た、汎用性のありそうな知見をまとめる。
- 要件・仕様・設計を分離した3層ドキュメント+決定の経緯ログをAIとの共同記憶として運用すると、セッションをまたいでも手戻りが減る。
- **試作(同一オリジン・軽量データ)→ 本番移植(GAS+スプレッドシート)**の2段階開発は有効だが、GAS特有の「iframeサンドボックス」制約は試作段階では顕在化しない。ネイティブAPI依存(
showPicker、localStorage同期、alert/confirm)は早めに自前実装へ倒しておくと移植コストが下がる。 - ブラウザ・OS差によるイベント発火順序の違い(Mac Safariの
blur/change)は、要素種別ごとに確定イベントを使い分けることでリスクを減らせる。 - データモデルは最初から正規化を狙わず、動くものを触りながら段階的に正規化する方が、業務要件の解像度が上がるスピードに追随できる。
- 複数画面で共有するロジックは早期に共通モジュールへ切り出し、画面固有差分は「フック」で吸収すると、集計バグの修正など横断的な変更が1箇所で完結する。
- オプトイン保存(即時反映・確定は別ボタン)は、スプレッドシートを裏で操作する業務アプリのUXの核になる。
本アプリは現在プロトタイプ完成・GAS本番移植を進めている段階。移植完了後の知見(同時編集の競合制御、レコード単位ロックなど)についても、機会があれば続編として書きたい。