SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Slack APIとGASでオフィス来客対応を20倍速!!

f:id:ug_23:20191011174449j:plain

スマートキャンプのエンジニアインターン生の高砂です! 高砂渉と書いて、たかすなじょうと読みます!(会社ではじょにーと呼ばれてます)

私はスマートキャンプでインターンを始めて半年ほど経ちますが、インターンを始めたばかりの頃、オフィス来客対応に非効率さを感じていました。

そこで、Slack APIとGASを使ってオフィス来客時の手動作業の自動化に取り組みました。

本記事ではどのように考えて、実装と改善をしていったかをお話ししようと思います!

来客対応における課題と要件

来客対応の手順

スマートキャンプでは基本的にインターン生がお客さまを会議室にお通ししていました。

お通しする際には「お客さまのお名前」「使用する会議室」「担当者の名前」が必要なのですが、従来では

  1. Slackに受付システムのbotから「お客さまのお名前」が通知される
  2. お名前に対応する予定をGoogleカレンダー上で目視で探す
  3. 見つけたら「使用する会議室」「担当者の名前」を記憶する
  4. お客さまを会議室に案内した後、担当者を呼び出す

というフローでした。

具体的な課題

上記手順では「目視で探す」「予定情報を記憶する」など間違いが起きやすいのに加え、一度のオペレーションに時間がかかる事が課題でした。

また、会社が成長中で日に日に社員が増えていく為、その負担が増していっているという問題もありました。

そこでこのフローを自動化したいと思い、趣味として取り組みました🙌

作成方針

主に以下のような流れで実装することにしました。

また、実用上の課題として、Receptionistに入力された名前や、カレンダーに入力された名前の漢字が間違っていることもあり、漢字のよみがなを取得できるよみたんというよみがな検索システムのAPIを用いています。

  1. お客様が受付で名前をReceptionistに入力
  2. ReceptionistがSlackに通知するのをGASでキャッチ
  3. Googleカレンダーから予定を検索
  4. よみがな検索APIで名前を
  5. Slackに通知

実装

GASで事前に今日1日の予定を取得

まずは会議室ごとに複数あるGoogleカレンダーの予定一覧を取得してきます。

この取得時に、よみたんのAPIを利用し、よみがなも取得しています。 また、この取得には時間がかかるので、業務時間開始1時間前に取得するようにトリガーを設定しました。

yomi-tan.jp

コードは以下のようになりました。

var scheduleSheetId = PropertiesService.getScriptProperties().getProperties().sheetId;
var spreadsheet = SpreadsheetApp.openById(scheduleSheetId);

var calendars = [
  CalendarApp.getCalendarById('カレンダーID1'),
  CalendarApp.getCalendarById('カレンダーID2'),
  CalendarApp.getCalendarById('カレンダーID3')
  ];

function main() {
  var scheduleList = setSheet(scheduleSheetId);

  var today    = new Date(new Date().toLocaleDateString());
  var tommorow = new Date(new Date().toLocaleDateString());
  today.setDate(today.getDate());
  tommorow.setDate(tommorow.getDate()+1);

  //複数のカレンダーから取得
  for (var room = 0; room < calendars.length; room++) {
    var lastRow = scheduleList.getLastRow()
    var events = calendars[room].getEvents(today, tommorow);
    var scheduleListValues = scheduleList.getDataRange().getValues();

    //予定の数だけ繰り返し
    for (var i = 0; i < events.length; i++) {
      var title     = events[i].getTitle();
      var startTime = events[i].getStartTime();
      var endTime   = events[i].getEndTime();
      var location  = events[i].getLocation();

      var patternList = [/.{2}様/g, /.{1}様/g, /.{3}様/g, /.{4}様/g]
      var kanaList = '';

      //訪問予定の場合
      if (/様/.test(title)) {
        patternList.forEach(function(pattern) {
          kanaList = arrangeScheduleInfo(pattern, title, kanaList)
        })

      //訪問予定では無い場合
      } else {
        var kanaList = [undefined]
      }

      var scheduleInfo = [title, startTime, endTime, location];
      scheduleList.appendRow(scheduleInfo);
    }
  }
}

function setSheet(scheduleSheetId) {
  var scheduleList = spreadsheet.getSheetByName('main');
  scheduleList.clearContents();

  var scheduleInfoTitle = ['予定名', '開始日時', '終了日時', '場所']
  scheduleList.appendRow(scheduleInfoTitle)

  return scheduleList
}

function arrangeScheduleInfo(pattern, title, kanaList) {
  var kanjiList = title.match(pattern);
  try {
    kanjiList.map(function(kanji) {
      var kanji = kanji.slice(0,-1);
      var kanaCandidates = translateIntoKanaArray(kanji);
      kanaList = kanaList + kanaCandidates + ',';
    })
  } catch (e) {
    kanaList = kanaList.concat(undefined);
    Logger.log('右記予定名はよみたん不可:' + title)
  }
  return kanaList
}

function translateIntoKanaArray(name) {
  var apiUrl = 'http://yomi-tan.jp/api/yomi.php?ic=UTF-8&oc=UTF-8&k=k&n=5&t=' + name;
  var kanaCandidates = UrlFetchApp.fetch(apiUrl);
  return kanaCandidates
}

Slack APIで訪問通知取得

実際にお客さまが来社された際、ReceptionistからSlackの特定のチャンネルにメッセージが送られます。

f:id:ug_23:20191011173105p:plain

このメッセージに対してSlack APIを反応させ、来客名を取得します。

var scheduleSheetId = PropertiesService.getScriptProperties().getProperties().sheetId;

function doPost(e) {
  var receptedName = extractName(e.parameter.text);
  var scheduleList = getActiveSheet(scheduleSheetId, 'main');
  searchSchedule(receptedName, scheduleList)
}

function extractName(receptedText) {
  var regex1 = / の.+? 様/;
  var regex2 = /から.+? 様/;
  var regex3 = /に、.+? 様/;
  if (regex1.test(receptedText)) {
    var name = receptedText.match(regex1);
  } else if (regex2.test(receptedText)) {
    var name = receptedText.match(regex2);
  } else if (regex3.test(receptedText)) {
    var name = receptedText.match(regex3);
  }

  name = String(name).slice(4,-3);
  return name
}

GASが対応する予定を検索

事前に取得していた予定一覧と照らし合わせて、対応する予定を決定します。

function searchSchedule(receptedName, scheduleList) {
  scheduleList.forEach(function(schedule) {
    var now           = new Date();
    var startTime     = new Date(schedule[1]);
    var earlyTime     = new Date(startTime.setMinutes(startTime.getMinutes() - 20));
    var lateTime      = new Date(startTime.setMinutes(startTime.getMinutes() + 20));

    if (earlyTime < now && now < lateTime) {
      nameMatchCheck(schedule, receptedName)
    }
  })
}

function nameMatchCheck(schedule, receptedName) {
  var scheduledNames = schedule[6];

  if (scheduledNames !== 'undefined') {
    var scheduledNamesList = scheduledNames.toString().split(',');
    scheduledNamesList.each(function(name) {
      if (!name) {
        return ;
      } else if (name.length === 1) { //1文字は誤判定が多いのでスキップ
        return ;
      } else if (receptedName.indexOf(name) === 0) { //名前一致
        postSchedule(schedule, name)
        return ;
      }
    })
  }
  return false;
}

Slack APIで予定を通知

f:id:ug_23:20191011173151p:plain

決定した予定の内容を通知します。

function postSchedule(scheduleInfo, scheduledName) {
  var name       = scheduleInfo[0];
  var startTime  = scheduleInfo[1].toLocaleTimeString().slice(0,-7);
  var endTime    = scheduleInfo[2].toLocaleTimeString().slice(0,-7);
  var place      = scheduleInfo[3];
  var guests     = scheduleInfo[5];
  var message = 'こちらの予定でしょうか:\n' + name + ' at ' + place + '\n' + startTime + ' ~ ' + endTime;
  postMessage(message)
}

function postMessage(message) {
  var postUrl = PropertiesService.getScriptProperties().getProperties().postUrl;
  var jsonData =
  {
    "username"  : "お茶たろう",
    "icon_emoji": ":johnny:",
    "text"      : message
  };
  var payload = JSON.stringify(jsonData);

  var options =
  {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload
  };

  UrlFetchApp.fetch(postUrl, options);
}

Slack botの反響と改善

これらによって、通知に対して情報を出してくれるSlack botが完成しました。

f:id:ug_23:20191011173042p:plain

このbotによって20秒程かかっていた手動での業務が1秒に短縮されました!

同僚の反応と追加機能の要望

反響も予想以上に多く、社員の方々から「お茶たろう(ツールの愛称)の人」と覚えられるまでになりました(笑)

それと同時に、更に色んな方から要望を頂けました。

  • もっと反応率を高めて欲しい
  • 1つの予定に対して複数のお客さまがいる場合も全員に対応して欲しい
  • 担当者をメンション(Slackの呼び出し機能)で呼び出して欲しい
  • 担当者が複数人いる場合、不参加の人は呼び出さないで欲しい
  • 口調を可愛くして欲しい

追加実装、利用、要望、ヒアリングのサイクル

その後も新しく貰った要望を基に実装し、実際に使い、それに対する要望を社員からヒアリングするという形式の開発を3ヶ月ほど続けました。

その結果、反応率は高く、社員の要望にも適合した良いプロダクトとなりました🤗

f:id:ug_23:20191011173023p:plain

終わりに

このbotに込めた想いは、スマートキャンプのPRメディアで話しているので良ければそちらもご覧ください。

スマートキャンプの「来客対応」の非効率を解決!内定者インターン生が開発した ”お茶たろう” に迫ってみた #Ownership #新卒 | .▲.tent