家庭用スケジュール最速登録ツール「サクカレ」設計図

AppSheet × Googleカレンダーで実現する家庭DX

【保存版】15分で完成!「サクカレ」構築完全マニュアル

こんにちは、もるです。
「春休みの家庭学習を10秒で見える化するアプリ」、サクカレに興味を持っていただきありがとうございます!

このマニュアルでは、プログラミングの知識がなくても「コピペとクリックだけ」で自分専用のカレンダー登録アプリを完成させる手順を解説します。


🚀 全体の流れ

  1. テンプレートをコピーする(アプリ本体 + スプレッドシート)
  2. マスター情報を書き換える(家族の名前、カレンダー設定)
  3. カレンダー同期(GAS)を設定する(魔法のコードを貼り付け)

準備はいいですか? それではいきましょう!


ステップ1:テンプレートを自分のGoogle環境にコピーする

まずは、アプリの「設計図」を自分のアカウントに取り込みます。

  1. こちらのテンプレートURL を開きます。
  2. 画面右上の 「Copy and customize」 をクリック。
  3. 「App name」(例:我が家のサクカレ)を入力し、「Copy app」 を押します。
AppSheetテンプレートのコピー画面
💡 NOTE これだけで、あなたのGoogleドライブにはアプリ本体と、データを保存するためのスプレッドシートが自動的に作成されます。

ステップ2:スプレッドシートの「マスター」を書き換える

アプリはコピーできましたが、中身がテンプレートの設定になっています。これをあなたのご家族用に直しましょう。

  1. AppSheetのエディタ画面の左メニュー 「Data」 を開き、「View data source」 をクリックします。
  2. Dataメニューからスプレッドシートを開く
  3. スプレッドシートが開くので、下のタブから 「Settings」 シートを編集します。
    • OwnerName: パパ、ママ、子供の名前など
    • CalendarID: その人のGoogleカレンダーのメールアドレス
    • Symbol: カレンダーに表示されるマーク(例:【パパ】、(兄)など)
    • Role: 自分(管理者)は Admin、その他は User
  4. 次に 「Master」 シートを編集します。
    • ここに「算数」「英語」「読書」「買い物」など、よく使うメニューを登録しておきます。
Settingsシート、Masterシートの設定例
⚠️ 重要:タイムゾーンの設定 カレンダーに登録される予定がずれないよう、スプレッドシートのタイムゾーンを確認しておきましょう。
スプレッドシートのメニュー「ファイル」>「設定」から、タイムゾーンを「(GMT+09:00) 東京」に変更してください。
タイムゾーンの変更
💡 TIP 「学習」以外のカテゴリ(生活など)の場合は、中分類(CategoryMid)を大分類と同じ名前にしておくと、アプリ側で入力の手間が省けますよ!

ステップ3:カレンダー同期(GAS)の設定

ここが一番の山場ですが、「コピペしてボタンを押すだけ」 です!

Apps Scriptの起動

① コードの貼り付け

  1. スプレッドシートのメニュー「拡張機能」> 「Apps Script」 をクリックします。
  2. もともと書いてあるコードをすべて消して、以下のコードをまるごと貼り付けてください。
/**
 * サクカレ同期システム (AppSheet連携 / テンプレート配布対応版)
 */

/**
 * 【重要】トリガーから呼び出すのはこの関数だけです
 */
function onAppSheetChange(e) {
  syncToCalendar();
}

/**
 * 同期処理のメインロジック
 */
function syncToCalendar() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('Schedule');
  const settingsSheet = ss.getSheetByName('Settings');
  
  if (!sheet) return;

  // 設定情報の読み込み(配布対応)
  let ownerConfig = {};
  if (settingsSheet) {
    const data = settingsSheet.getDataRange().getValues();
    // 1行目は見出し [表示名, カレンダーID, 記号]
    for (let i = 1; i < data.length; i++) {
      const name = data[i][0];
      const calId = data[i][1];
      const symbol = data[i][2];
      if (name) {
        ownerConfig[name] = { id: calId, symbol: symbol || '●' };
      }
    }
  }

  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  const range = sheet.getRange(2, 1, lastRow - 1, 10); // A列〜J列まで
  const values = range.getValues();

  values.forEach((row, index) => {
    const rowNum = index + 2;
    
    // カラム定義
    const categoryMid = row[2]; // C
    const subject = row[3];     // D
    const date = row[4];        // E
    const startTimeStr = row[5];// F
    const minutes = row[6];     // G
    const details = row[7];     // H
    const status = row[8];      // I
    const email = row[9];       // J
    
    // 同期対象チェック: Statusが「送信待ち」の行のみ処理
    if (status !== '送信待ち') return;
    
    // 所要時間の計算ロジック (G列: Minutes)
    // 1. まず「数字のみ」の項目を足し算する (Base)
    // 2. 「x」や「×」を含む数字を掛け算する (Multiplier)
    const minutesValue = row[6] ? row[6].toString() : "0";
    const parts = minutesValue.split(/[,、]/).map(p => p.trim());
    let baseMinutes = 0;
    let multiplier = 1;

    parts.forEach(p => {
      const num = parseInt(p.replace(/[^0-9]/g, ''));
      if (isNaN(num)) return;
      if (p.includes('x') || p.includes('×') || p.includes('X') || p.includes('*')) {
        multiplier *= num;
      } else {
        baseMinutes += num;
      }
    });
    const totalMinutes = baseMinutes * multiplier;
    if (!email || !date) return;

    try {
      // Email (J列) をキーに設定情報を取得
      const config = ownerConfig[email] || { id: email, symbol: '●' };
      const calendarId = config.id || email || Session.getEffectiveUser().getEmail();
      const symbol = config.symbol || '●';
      
      const calendar = CalendarApp.getCalendarById(calendarId);
      if (!calendar) throw new Error('カレンダーにアクセスできません: ' + calendarId);

      // 日時結合
      const startDateTime = combineDateAndTime(date, startTimeStr);
      if (!startDateTime) throw new Error('日時の形式不正');

      // 所要時間計算
      // let totalMinutes = calculateTotalMinutes(minutes); // Old calculation
      const endDateTime = new Date(startDateTime.getTime() + (totalMinutes * 60 * 1000));

      // タイトル生成(記号+中分類 or 小分類)
      const eventTitle = `${symbol}${categoryMid || subject}`;
      const eventDescription = `【${subject}】\n${details || ""}`;

      // 登録
      const event = calendar.createEvent(eventTitle, startDateTime, endDateTime, {
        description: eventDescription
      });

      // 色設定
      setEventColorBySubject(event, subject);

      // 同期完了マーク(I列=9列目)
      sheet.getRange(rowNum, 9).setValue('送信完了'); 
      console.log(`成功: ${email} (${calendarId}) に登録完了`);
      
    } catch (error) {
      console.error(`Row ${rowNum} エラー: ${error.message}`);
    }
  });
}

/**
 * 日付と時間を結合してJS Dateオブジェクトを返す
 */
function combineDateAndTime(dateValue, timeValue) {
  if (!dateValue) return null;
  let date = new Date(dateValue);
  if (isNaN(date.getTime())) return null;
  
  let hours = 0;
  let minutes = 0;
  
  if (timeValue instanceof Date) {
    const timeStr = Utilities.formatDate(timeValue, Session.getScriptTimeZone() || "JST", "HH:mm");
    const parts = timeStr.split(':');
    hours = parseInt(parts[0], 10);
    minutes = parseInt(parts[1], 10);
  } else if (timeValue) {
    const timeStr = String(timeValue);
    const match = timeStr.match(/(\d{1,2}):(\d{1,2})/);
    if (match) {
      hours = parseInt(match[1], 10);
      minutes = parseInt(match[2], 10);
    }
  }
  
  date.setHours(hours, minutes, 0, 0);
  return date;
}

/**
 * 小分類(Subject)に応じてカレンダーイベントの色を設定する
 * (GoogleカレンダーのColor ID: 1〜11 を使用)
 */
function setEventColorBySubject(event, subject) {
  if (!event || !subject) return;

  // --- 色設定のカスタマイズ ---
  const colorMap = {
    '国語': '11',  // トマト(赤)
    '算数': '7',   // ピーコック(ターコイズ)
    '英語': '4',   // フラミンゴ(薄赤)
    '理科': '10',  // バジル(緑)
    '社会': '5',   // バナナ(黄)
    '4教科': '6', // ミカン(橙)
    'ピアノ': '9', // ブルー(青)
    '授業': '2',   // セージ(薄緑)
    'その他': '8'  // グラファイト(グレー)
  };
  // -------------------------

  const colorId = colorMap[subject];
  if (colorId) event.setColor(colorId);
}

/**
 * 【参考】Googleカレンダー Color ID と色の対応
 * 1: ラベンダー (薄紫), 2: セージ (薄緑), 3: ブドウ (紫)
 * 4: フラミンゴ (薄赤), 5: バナナ (黄), 6: ミカン (橙)
 * 7: ピーコック (水色), 8: グラファイト (グレー), 9: コバルト (青)
 * 10: バジル (緑), 11: トマト (赤)
 */
  1. 貼り付けたら、フロッピーアイコン(またはCtrl+S)で保存します。
コードの貼り付けと保存 トリガー設定メニュー トリガーの追加ボタン

② 「トリガー(自動実行)」の設定

アプリからデータが届いた瞬間に、このコードが動くように設定します。

  1. 左側の 「時計アイコン(トリガー)」 をクリック。
  2. 右下の 「+ トリガーを追加」 をクリック。
  3. 設定を以下のようにします:
    • 実行する関数:onAppSheetChange
    • イベントのソース:スプレッドシートから
    • イベントの種類:変更時
  4. 「保存」を押します。初回はGoogleの承認画面が出るので、自分のアカウントを選んで「許可(Allow)」してください。
トリガーの詳細設定

🏁 完成!テストしてみよう

  1. スマホのAppSheetアプリで適当な予定を入れて 「保存(Save)」 します。
  2. 画面上部の 「カレンダー同期」 ボタン(紙飛行機アイコン)を押します。
  3. 10秒ほど待って、Googleカレンダーに予定が現れれば……大成功です! 👏
⚠️ IMPORTANT うまく同期されない場合は、スプレッドシートの Status 列が「送信待ち」のまま止まっていないか確認してください。

おわりに

これであなたの家庭専用・爆速カレンダー登録環境は完成です!
最初は設定に少し手間がかかりますが、一度作ってしまえば日々の運用は驚くほど楽になります。

もし分からないことがあれば、NoteのコメントやX(旧Twitter)でお気軽にご相談くださいね。

それでは、素敵な家庭DXライフを!🎮✨