8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GAS × Antigravity】カンバン形式タスク管理ツールを生成AIと壁打ちして作った話

8
Posted at

はじめに

「自分のTODO、ちゃんと把握できてますか?」

Google Tasks, Notion, Asana…世の中にはタスク管理ツールが溢れています。でも「自分の使い勝手に完全に合うもの」というのは、なかなか見つかりません。私もそうでした。

作った理由は3つです。

  1. UIの問題: Google Tasksは自分の使い勝手に合わなかった。「把握しやすく、毎日開いて更新したくなるUI」が欲しかった
  2. データの安心感: SaaSに頼ると、サービス終了・仕様変更のリスクがある。スプレッドシートにデータがあれば、いざとなれば自分でどうにでもできる
  3. AIとの壁打ちで作れる規模感: このくらいの個人ツールなら、生成AI(Antigravity)の無料範囲でも十分に作れると判断した

そして完成したのが MyTask です。


完成したもの

まずは見てください。
こちらがWebアプリの画面です。
13_testdata_webapp.png
こちらが、データのSpreadSheetです。
13_testdata_spreadsheet.png

主な機能:

  • カンバンボード: 「未着手」「実施中」「保留」「完了」の4カラム、ドラッグ&ドロップでステータス変更
  • リッチテキストエディタ: 太字・斜体・文字色などの装飾がWebとSpreadsheet間で双方向に同期
  • フィルタ&ソート: グループ・優先度・期限でのフィルタリングと並び替え
  • Google Tasks連携: カレンダーに登録したタスクを一括インポート
  • アクセス制御: メールアドレスで許可ユーザーを管理

GitHubはこちら → https://github.com/blueshooterX/MyTask

Github上ではコードの可読性や生成AIとの相性を考慮し、機能別にファイルを分割していますが、完成したコードをdist/Code.gsdist/index.html の2ファイルに結合しています。
この2ファイルをGASエディタに貼り付けるだけでデプロイできるので、技術的な背景がない方でも導入できます。(導入手順は上記GithubのREADMEをご確認ください。)


技術スタック

Backend  : Google Apps Script (V8 Runtime) 
Database : Google Spreadsheet
Frontend : HTML5 / Vanilla CSS / ES6+ JavaScript
Library  : Sortable.js, Material Icons, Google Fonts
Dev Tool : clasp(開発)+ 自作 bundler.js(配布用)

アーキテクチャ概要:

技術ハイライト①:リッチテキスト同期という鬼門

これが一番苦労した実装です。

何がしたかったか

タスクの内容欄をリッチテキストエディタにしたかった。太字・文字色・斜体などで情報を強調できます。

さらに、その装飾情報をスプレッドシートのセルにもそのまま保存したい。スプレッドシートを開けばセルの中の文字が色付きで見える状態にしたかった。

なぜ難しいのか

GoogleスプレッドシートのAPIには RichTextValue という専用オブジェクトがあります。これはプレーンテキストに対し「何文字目から何文字目まで太字」という文字インデックスで範囲指定してスタイルを設定する仕組みです。

問題は、WebブラウザのHTML(contenteditable DIV)と、このインデックス管理の世界が根本的に相性が悪いことです。

  • <br><div> などのブロック要素が改行として扱われる
  • ブラウザがHTMLに余計なタグを挿入することがある
  • 太字と色が隣接する場合のタグのネスト順序

インデックスが1文字でもズレると装飾がまるごと壊れます。

突破口

最初はなかなか上手くいきませんでした。そこで、以前自分で書いたQiita記事を生成AIに参照させることで突破できました。

参考にしたのはこちら

「スプレッドシートのRichTextは、こういうオブジェクト構造で、このAPIで操作する」という基礎知識をAIに理解させてから実装を依頼することで、議論の質が一気に上がりました。

解決策:RichTextLib.js

最終的な実装の核はこの2つの関数です。

スプレッドシート → HTML(読み込み時):

// RichTextLib.js: richTextToHtml
function richTextToHtml(rtv) {
    const text = rtv.getText();
    const runs = rtv.getRuns(); // 同一スタイル単位の塊を取得

    let html = "";
    runs.forEach(run => {
        let runText = run.getText();
        // HTMLエスケープ処理...
        
        const style = run.getTextStyle();
        let taggedText = runText;

        // スタイルに応じてタグを付与
        if (style.isBold()) taggedText = `<b>${taggedText}</b>`;
        if (style.isItalic()) taggedText = `<i>${taggedText}</i>`;
        if (style.isStrikethrough()) taggedText = `<s>${taggedText}</s>`;
        if (style.isUnderline()) taggedText = `<u>${taggedText}</u>`;

        const color = style.getForegroundColor();
        if (color && color !== '#000000') {
            taggedText = `<span style="color:${color}">${taggedText}</span>`;
        }
        html += taggedText;
    });

    return html.replace(/\r\n|\r|\n/g, "<br>");
}

getRuns() は「同一スタイルの文字の塊」を返してくれるので、各runに対してHTMLタグを付与することで確実に再現できます。

HTML → スプレッドシート(保存時):

フロントエンドのリッチテキストエディタがDOMツリーを解析し、各テキスト区間のスタイル情報を runs 配列として生成します。バックエンドの htmlToRichText 関数はこの配列をもとに RichTextValueBuilder を使って精密にインデックスを管理しながらオブジェクトを構築します。

// charStylesで1文字ずつスタイルを管理し、
// スタイルの変わり目でrunを区切ってbuilder.setTextStyle()を呼ぶ
for (let i = 1; i <= fullPlainText.length; i++) {
    if (currentStyleStr !== lastStyleStr) {
        // ここでスタイルの変わり目にsetTextStyleを適用
        if (hasStyle) builder.setTextStyle(runStart, i, textStyle.build());
        runStart = i;
    }
}

1文字ごとにスタイルを配列で管理し、スタイルの変わり目でのみAPIを呼び出すことで、インデックスのズレを完全に排除しています。


技術ハイライト②:GASでもモダンなUIは作れる

UIはVanilla CSSで作成し、細かい点を生成AIと微調整する方法としています。

GASは最終的にHTMLをクライアントのブラウザで表示するだけなので、Vanilla CSSで何でもできます。(ただし、CSSの記述が長くなってしまいました・・・。)

Glassmorphism

フロスト加工されたガラスのような質感のデザインです。

.card {
    background: rgba(255, 255, 255, 0.08);
    backdrop-filter: blur(16px);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 12px;
}

backdrop-filter: blur() と半透明背景の組み合わせだけで実現できます。

ドラッグ&ドロップ(Sortable.js × google.script.run)

GASとのドラッグ&ドロップ連携のポイントは非同期通信です。Sortable.jsのドロップ完了コールバックでDOMの順序から新しいステータスと表示順を計算し、google.script.run でバックグラウンド保存します。

// ドロップ完了時に発火
onEnd: function(evt) {
    const newStatus = evt.to.dataset.status; // ドロップ先のカラムのステータス
    const taskId = evt.item.dataset.id;
    
    // GASに非同期送信(画面ブロックなし)
    google.script.run
        .withSuccessHandler(onSaveSuccess)
        .updateOrderAndStatus(orderData);
}

ユーザーはUIを操作し続けられ、保存は裏で行われます。

ファイル分割による保守性確保

GASはHTMLファイルのインクルードが使えます。

<!-- index.html -->
<?!= include('css'); ?>
<?!= include('js-main'); ?>
<?!= include('js-kanban-view'); ?>

機能ごとにファイルを分割することで、1000行超のモノリシックファイルにせず、生成AIが必要なファイルのみを参照できるようにしました。


AIとのペアプロ開発体験(Antigravity)

今回の開発はほぼ全て、生成AI(Antigravity)とのペアプロで進めました。

役割分担

担当 内容
私(人間) 要件の定義・仕様の意図・方向性の判断
AI(Antigravity) 実装コードの生成・デバッグ・リファクタリング提案

効果的だった使い方

日本語の要件をそのまま投げる。
「グループ名のハッシュ値から一意な色を自動生成して、バッジのborder-colorに使いたい」といった要望を自然言語で伝えるだけで実装してくれます。

参考情報をコンテキストとして渡す。
前述の通り、参考にして欲しい情報を具体的に渡す方法が効果的でした。

難しかった点

UIの挙動や見た目の要望を伝えるのは、自分で言語化する必要があり、若干の難しさを感じました。(画像で済むところは画面キャプチャして読み取ってもらうことができたので、表示部分は助かりました。)


おわりに

GASと生成AIでのツール作成は親和性が非常に高いと感じています。

  • 認証・インフラ・データベース管理をGoogleが全て担ってくれる(コードがコンパクト)
  • フロントエンドはHTMLを書けばよいので、AIがコードを生成しやすい
  • Antigravity + claspによるデプロイ環境を作れば、コード改修からデプロイ・試行しやすい

まずAIとの壁打ちでプロトタイプを作り、そこから磨いていくアプローチは、個人開発において非常に有効です。

今回作ったMyTaskはGitHubで公開しています。

👉 https://github.com/blueshooterX/MyTask

AIに本リポジトリのURLを渡して「こういう機能を追加したい」と伝えれば、あなたの使い勝手に合わせてカスタマイズできます。ぜひ使ってみてください。

8
9
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
8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?