会食で人を覚えられないのは当たり前。「会食メモ × リマインド通知」で人を覚える仕組みを作ってみた

GAS

最近、会食が多く、初めましての方と話すことが多くありました。折角、話が弾んでも情報を覚え続けられないことが多く、この間も同じような話しましたよね?的な雰囲気になってしまったことが多々ありました。

そこで、簡単に何か便利なものができないか?と思い立ち、作ってみたのが、この通知システムです。

スポンサーリンク
スポンサーリンク

「会食メモ」という考え方

会食メモとは、会食や打ち合わせの内容を最低限記録し、次回に活かすためのメモです。

難しいことは不要です。

Googleフォーム(3分入力)

メール通知例

会食メモに書くべき最低限の項目

  • 相手の名前・会社
  • 会った日付
  • 場所(店名など)
  • 話した話題
  • 次につながる一言
  • 好み・NG話題(あれば)

これだけで十分です。

「会食メモ × リマインド」という仕組み

そこで私が作ったのが、
会食メモ × リマインドの仕組みです。

何ができるのか?

  • 会食後、スマホで3分入力
  • 会食メモを一元管理
  • 次回の会食予定が近づくと前回の要点が自動で届く

「思い出す努力」を一切しなくて済みます。

仕組みは驚くほどシンプル

使っているのは、すべて無料のツールです。

  • Googleフォーム(会食後の入力)
  • Googleスプレッドシート(データ管理)
  • Googleカレンダー(予定管理)
  • Google Apps Script(自動処理)

流れはこうです。

会食後
 ↓
Googleフォームで会食メモ入力(3分)
 ↓
スプレッドシートに自動保存
 ↓
次回の会食予定が近づく
 ↓
前回の会食メモがメールで届く

実際に使って感じた変化

この仕組みを使い始めてから、明確に変わりました。

  • 会食前の不安がなくなった
  • 会話が自然につながる
  • 「覚えてくれているんですね」と言われる
  • 地雷を踏まなくなった

結果として、
人間関係に使うエネルギーが激減しました。何よりもその人のことを知りたいと前向きになりました。

会食メモは営業ツールではない

誤解されがちですが、
会食メモは営業職専用のものではありません。

  • 社内の打ち合わせ
  • 上司・部下との面談
  • 取引先との会食
  • 外部パートナーとのミーティング

人と継続的に関わるすべてのビジネスパーソンにおすすめです。

初心者でも続けられる理由

この仕組みが続く理由はシンプルです。

  • 入力は「会食後に3分」
  • 思い出す作業は「自動」
  • 脳の負担がほぼゼロ

「頑張らなくても回る」ことが、最大の価値です。

完全版について

上記では考え方と全体像を解説しました。

実際に使っている完全版では、

  • 会食メモ用Googleフォーム
  • 人物一覧から自動で候補が更新される仕組み
  • 自動リマインドスクリプト
  • 初心者向けの設定手順

まで含めています。

そのまま使える形でまとめていきます。

会食メモ~リマインド作成

それでは早速、作成していきましょう。

0) 使い方(概要)

  1. Googleスプレッドシートを作成
    • People / Meetings の2シートを作り、ヘッダー行を入れる
  2. Apps Scriptを開いて下記コード貼り付け
  3. SPREADSHEET_ID を設定(シートURLの/d//editの間)
  4. setupTrigger() を1回実行(権限許可)
  5. カレンダーにイベントを入れる
    • 接待:山田 太郎(○○商事) 形式
  6. 毎朝9時に自動でブリーフメールが届く

1) MVPのゴール(最初に作るもの)

できること

  • 接待相手(人物)を登録・更新
  • 接待(会食)予定を登録(Googleカレンダーに入れる)
  • 予定日の前日(または当日朝)に、相手の要点をメールで自動通知
    • 前回話した内容
    • NG話題
    • 好み(酒・食・話題)
    • 次回のおすすめ話題(任意)

2) データベース設計(スプレッドシート)

スプレッドシートに以下の2シートを作ります。

シート① People シート 1行目(A1〜)

以下を 1行目にそのまま貼り付け(タブ区切り):

person_id	name	name_kana	company	title	relationship_level	likes_food	likes_drink	likes_topics	ng_topics	notes	updated_at

シート② Meetings シート 1行目(A1〜)

meeting_id	date	person_id	place	attendees	topics	takeaway	mood	next_action	created_at
Peopleにまず1人だけ登録(動作確認用)

Peopleの2行目に例として入力(タブ区切りコピペOK):

P0001	山田 太郎	やまだ たろう	○○商事	部長	A	鮨・和食	ハイボール	ゴルフ・投資	政治・家庭	前回は新規案件の相談。次回は見積の方向性確認。	

updated_at は空でOK(後で自動更新に拡張可能)

3) 予定(カレンダー)側の運用ルール

イベントタイトル例

Googleカレンダーのイベントタイトルを以下の形式にします。

例:
接待:山田 太郎(○○商事)
または
会食:山田 太郎

GAS側はタイトルから 氏名を抽出し、Peopleに一致させます。
(氏名一致の精度を上げるため、Peopleのnameは「カレンダー表記と同じ」にするのがコツ)

デフォルトでのタイトルの先頭キーワードの許容例は「接待」「会食」「面談」「打合せ」「商談」

になります。

4) GAS構成(ファイルとトリガー)

実行タイミング

  • 毎日 09:00 に定期実行
    → 24時間以内の接待イベントを探して通知

通知時間は変更可能です。後述を参照下さい。

GASの主な処理

  1. 今日〜24時間のカレンダーイベントを取得
  2. イベントタイトルから相手名を抽出
  3. Peopleから該当人物を検索
  4. 最新のMeetingsログを1件引く
  5. まとめてメール送信(自分宛)

5) Apps Scriptを作成してコード貼り付け

  1. スプレッドシート上部:拡張機能 → Apps Script
  1. 新規プロジェクトに、GASコードを貼り付け
  1. コード内の以下を設定:
    • SPREADSHEET_ID = "ここにスプレッドシートID";

SPREADSHEET_IDの取得方法は以下になります。

スプレッドシートURLの/d//edit の間の文字列がIDです。

GAS実装コード

以下を Apps Script に貼り付けて使えます。
(※ SPREADSHEET_IDCALENDAR_ID はあなたの環境に合わせて設定)

/**
 * 接待メモ × リマインド(個人版A)
 * - Google Calendarの予定(接待/会食)から相手を抽出
 * - People / Meetingsから情報を引く
 * - 自分宛に事前ブリーフィングをメール送付
 * - 二重通知防止(同一イベントを一度だけ通知)
 */

/** ===== 設定 ===== */
const SPREADSHEET_ID = "ここにスプレッドシートID";
const CALENDAR_ID = "primary";
const NOTIFY_TO = Session.getActiveUser().getEmail();
const LOOKAHEAD_HOURS = 30; // 前日夜〜当日も拾えるよう少し長め
const SUBJECT_PREFIX = "【接待ブリーフ】";

/** ===== シート名 ===== */
const SHEET_PEOPLE = "People";
const SHEET_MEETINGS = "Meetings";

/**
 * 毎日実行(推奨:09:00)
 */
function dailyBriefing() {
  const now = new Date();
  const end = new Date(now.getTime() + LOOKAHEAD_HOURS * 60 * 60 * 1000);

  const cal = CalendarApp.getCalendarById(CALENDAR_ID);
  const events = cal.getEvents(now, end);
  if (!events || events.length === 0) return;

  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const people = loadPeople_(ss);
  const meetings = loadMeetings_(ss);

  events.forEach(ev => {
    const title = ev.getTitle();
    const start = ev.getStartTime();

    // 「接待:氏名(会社)」などから氏名を抽出
    const personName = extractPersonName_(title);
    if (!personName) return;

    const person = findPersonByName_(people, personName);
    if (!person) return;

    // 二重通知防止キー
    const notifyKey = buildNotifyKey_(person.person_id, start);
    if (wasNotified_(notifyKey)) return;

    const lastMeeting = findLastMeeting_(meetings, person.person_id);

    const subject = `${SUBJECT_PREFIX}${person.name} / ${formatDateTime_(start)}`;
    const body = buildBriefBody_(person, lastMeeting, title, start);

    GmailApp.sendEmail(NOTIFY_TO, subject, body);

    markNotified_(notifyKey);
  });
}

/** ====== データ読込 ====== */
function loadPeople_(ss) {
  const sh = ss.getSheetByName(SHEET_PEOPLE);
  const values = sh.getDataRange().getValues();
  values.shift(); // header

  return values
    .filter(r => r[0] && r[1])
    .map(r => ({
      person_id: r[0],
      name: r[1],
      name_kana: r[2],
      company: r[3],
      title: r[4],
      relationship_level: r[5],
      likes_food: r[6],
      likes_drink: r[7],
      likes_topics: r[8],
      ng_topics: r[9],
      notes: r[10],
      updated_at: r[11],
    }));
}

function loadMeetings_(ss) {
  const sh = ss.getSheetByName(SHEET_MEETINGS);
  const values = sh.getDataRange().getValues();
  values.shift(); // header

  return values
    .filter(r => r[0] && r[2])
    .map(r => ({
      meeting_id: r[0],
      date: r[1],
      person_id: r[2],
      place: r[3],
      attendees: r[4],
      topics: r[5],
      takeaway: r[6],
      mood: r[7],
      next_action: r[8],
      created_at: r[9],
    }));
}

/** ====== タイトル解析 ====== */
function extractPersonName_(title) {
  // 許容例:
  // "接待:山田 太郎(○○商事)"
  // "会食: 山田太郎"
  // "面談:山田 太郎"
  // → 先頭キーワード + ":" or ":" を見て抽出
  const m = title.match(/^(接待|会食|面談|打合せ|商談)\s*[::]\s*([^\((]+)(?:[\((].*[\))])?$/);
  if (!m) return null;
  return m[2].trim();
}

function findPersonByName_(people, name) {
  const norm = normalize_(name);
  return people.find(p => normalize_(p.name) === norm) || null;
}

function findLastMeeting_(meetings, personId) {
  const list = meetings
    .filter(m => m.person_id === personId)
    .sort((a, b) => new Date(b.date) - new Date(a.date));
  return list[0] || null;
}

/** ====== メール本文 ====== */
function buildBriefBody_(person, lastMeeting, eventTitle, eventStart) {
  const L = [];
  L.push(`予定:${eventTitle}`);
  L.push(`日時:${formatDateTime_(eventStart)}`);
  L.push("");

  L.push("■ 相手(要点)");
  L.push(`- 氏名:${person.name}${person.name_kana ? `(${person.name_kana})` : ""}`);
  L.push(`- 所属:${joinNonEmpty_([person.company, person.title], " / ") || "—"}`);
  L.push(`- 重要度:${person.relationship_level || "—"}`);
  L.push("");

  L.push("■ 好み・地雷");
  L.push(`- 食:${person.likes_food || "—"}`);
  L.push(`- 酒:${person.likes_drink || "—"}`);
  L.push(`- 話題:${person.likes_topics || "—"}`);
  L.push(`- NG:${person.ng_topics || "—"}`);
  L.push("");

  L.push("■ 前回(直近ログ)");
  if (lastMeeting) {
    L.push(`- 日付:${formatDate_(lastMeeting.date)}`);
    L.push(`- 場所:${lastMeeting.place || "—"}`);
    L.push(`- 話題:${lastMeeting.topics || "—"}`);
    L.push(`- 要点:${lastMeeting.takeaway || "—"}`);
    L.push(`- 次アクション:${lastMeeting.next_action || "—"}`);
  } else {
    L.push("- 記録なし(初回 or 未入力)");
  }
  L.push("");

  L.push("■ メモ");
  L.push(person.notes || "—");

  return L.join("\n");
}

/** ====== 二重通知防止 ====== */
function buildNotifyKey_(personId, startDate) {
  const day = Utilities.formatDate(startDate, Session.getScriptTimeZone(), "yyyyMMdd");
  const time = Utilities.formatDate(startDate, Session.getScriptTimeZone(), "HHmm");
  return `brief_${personId}_${day}_${time}`;
}

function wasNotified_(key) {
  const props = PropertiesService.getScriptProperties();
  return props.getProperty(key) === "1";
}

function markNotified_(key) {
  const props = PropertiesService.getScriptProperties();
  props.setProperty(key, "1");
}

/**
 * 1回だけ実行:毎日09:00のトリガー作成
 */
function setupTrigger() {
  ScriptApp.newTrigger("dailyBriefing")
    .timeBased()
    .everyDays(1)
    .atHour(9)
    .create();
}

/**
 * デバッグ用:通知履歴を全削除(必要なときだけ)
 */
function resetNotifiedFlags() {
  const props = PropertiesService.getScriptProperties();
  const all = props.getProperties();
  Object.keys(all)
    .filter(k => k.startsWith("brief_"))
    .forEach(k => props.deleteProperty(k));
}

/** ====== util ====== */
function normalize_(s) {
  return (s || "").replace(/\s+/g, "").trim();
}
function joinNonEmpty_(arr, sep) {
  return arr.filter(v => v && String(v).trim()).join(sep);
}
function formatDateTime_(d) {
  return Utilities.formatDate(d, Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm");
}
function formatDate_(d) {
  if (!d) return "—";
  const dd = (d instanceof Date) ? d : new Date(d);
  return Utilities.formatDate(dd, Session.getScriptTimeZone(), "yyyy-MM-dd");
}
スクリプトを貼り付け

6) トリガーを1回だけ設定

Apps Scriptの関数一覧から setupTrigger() を実行

権限許可(Gmail/Calendar/Sheets)を進める

7) Googleカレンダーに予定を入れる(重要:タイトル形式)

タイトルは必ずこの形式にします:

  • 接待:山田 太郎(○○商事)
  • 会食:山田 太郎

開始日時は、明日以降でも当日でもOKです。
(スクリプトは「今から30時間以内」を見て通知します)

8) 動作確認(すぐ試す)

Apps Scriptで dailyBriefing() を選択→「▷実行」をクリック。

自分宛に **【接待ブリーフ】**メールが届けば成功です。

運用ルール(最短で回る)

  • 会食後にMeetingsへ1行だけ入れる(3分)
    • date / person_id / place / topics / takeaway / next_action だけ埋めれば十分
  • 次回の予定が入ると、前日(または当日)にメールで要点が届く

まとめ

ここでは、最小版の仕組みを紹介しました。次回は、Googleフォームからメモ内容をスプレッドシートに転記する機能を紹介します。

タイトルとURLをコピーしました