毎日出社できるオフィスがない弊社が、社内コミュニケーションを活性化させるための施策をGASでサクッと作ってみた


こんにちは!Offers を運営している株式会社 overflow のバックエンドエンジニアの Shun です。
タイトルの通り、overflow 社には毎日出社できるオフィスがありません。。
というのも、コロナ禍で物理的な出社が厳しくなったことにより、オフィスを引き払う決断をしたためです。

参考URL

https://note.com/suzukiyuto/n/n8a1c24989668

通勤ストレスや社内での移動など、人によってはストレスな要因が減りはしましたが、同時に社内の業務以外のコミュニケーションも減っているように感じました。
なので、テクノロジーを使用して解決できないかと思い、社内コミュニケーションを活性化させるための方法を GAS(Google Apps Script)で実装してみることにしました。

実際に社内に取り入れたもの

Breaking Timeとは

簡単にいうと「社内で仕事以外のコミュニケーションを取るための仕組み」というものです。
オフィスにいる際は、廊下でたまたま出くわした社員と立ち話をしたり、ランチに行ったりして仕事以外のコミュニケーションをとっていましたが、私たちは出くわすことすらできないのです。
なので、毎週月曜日に固定で予定をスケジュールし、定期的なコミュニケーションをとれるようにしました。

いざ実装!

全体設計

まずは全体的な設計。一般的なよくある設計でございます。
処理の流れの図示

具体的な処理の流れ

  1. スプレッドシートの社員名簿から、朝会参加フラグが立った行データを取得
  2. 役員と正社員は MUST 参加とし、3 人ずつグルーピング
  3. 正社員以外のメンバーを後から 1 人ずつグループへ追加
  4. グループごとに GoogleMeet URL 発行
  5. トークテーマをランダムで決定
  6. Slack へ通知
  7. 時間になったらグループ毎に Breaking Time!

最終的に、以下のような投稿が Slack に POST されます。
SlackでBraking Timeの案内が自動投稿されているスクリーンショット

実際にやってみてどうだったか

結論として、以下のような意見をいただくことができました。

自分はいろんな人と話すのが割と好きなので、第一に楽しいし、仕事の活力になると感じています!リモートで働く中でも組織に属している実感が得られる場なので、個人的にとても好きな時間です。
新卒内定者(現インターン)の南方さん

私個人的にも、普段顔を合わせる時には業務の話しかしないので、このような時間があるとリラックスできるかつ、メンバーの人となりが分かる良い機会だと思っています。

今後、社内コミュニケーション解決のために実践したいこと

最近流行りのメタバースで出社してみたい感はありますが、現実世界ではみんな Oculas2 を被って仕事しないといけないので現実的ではなさそうですね笑
今回の施策で出てきたフィードバックなどを反映させていき、質の向上をまずは行っていきたいと思います。

まとめ

今の世の中的に、働き方が大きく変わった組織も増えてきたと思いますが、やはり「組織 = 人の集合」であることを忘れてはいけないなと感じております。
仕事の前に、相手を最低限は理解した上で接することで社内のモチベーション向上にも繋がると思いますので、組織的に黄色信号を感じたらすかさずジャストアイデアを提案していこうと思います。
もし「私の会社では社内コミュニケーションをこうやって解決しています!!」ということがあれば、是非ともコメント欄にコメントをお願いします!

(付録)実際に稼働させてるプログラム

もしこのプログラムがお役に立てば嬉しいので、貼っておきます。

メイン処理
Main.gs
const main = () => {
  const targetMembers = getTargetMembers()
  const mustMembers = targetMembers.filter(member => {
    return ['正社員', '役員'].includes(member[header.indexOf('雇用形態')])
  }).map(members => members[header.indexOf('名前')])

  const wantMembers = targetMembers.filter(member => {
    return !['正社員', '役員'].includes(member[header.indexOf('雇用形態')])
  }).map(members => members[header.indexOf('名前')])

  // 正社員で3人ずつグルーピングする
  const mustSliced = sliceByNumber(mustMembers, 3, 3)
  const shuffledWants = shuffle(wantMembers)

  // 正社員グループにwantメンバーを一人ずつ入れていく
  mustSliced.reverse().forEach((a, i) => {
    if(shuffledWants.pop()) {
      a.push(shuffledWants.pop())
    }
  })

  const needInfo = generateNeedInfo(mustSliced)
  const messageBlock = generateMessageBlock(needInfo)

  notificationToSlack(messageBlock, GENERAL_ACTIVE)

  return true
}
Utils
Utils.gs
// 毎週月曜日の夜中に、10:00にトリガー設定する
const setTrigger = () => {
  const time = new Date()
  time.setHours(10)
  time.setMinutes(00)
  ScriptApp.newTrigger('main').timeBased().at(time).create()
}

const sliceByNumber = (array, number, minimum) => {
  const shuffled = shuffle(array)
  const length = Math.ceil(shuffled.length / number)
  const slicedArr = new Array(length).fill().map((_, i) =>
    shuffled.slice(i * number, (i + 1) * number)
  )

  const lastElem = slicedArr[slicedArr.length - 1]
  if(lastElem.length < minimum) {
    for(let i=0; i<lastElem.length; i++) {
      slicedArr[i].push(lastElem[i])
    }
    slicedArr.pop()
  }

  return slicedArr
}

const shuffle = array => {
  let currentIndex = array.length, randomIndex;

  while (currentIndex != 0) {
    randomIndex = Math.floor(Math.random() * currentIndex)
    currentIndex--

    [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]
  }

  return array
}

const getMeetUrl = () => {
  const calendarId = 'primary'
  const dt = new Date()
  const date = dt.getFullYear() + '-' + (dt.getMonth() + 1) + '-' + dt.getDate()
  const requestId = Math.random().toString(32).substring(2)
  const response = Calendar.Events.insert({
    summary: "breakingTime",
    singleEvents: true,
    allDayEvent: true,
    start: { date },
    end: { date },
    conferenceData: {
      createRequest: {
        requestId,
        conferenceSolutionKey: {
          type: "hangoutsMeet"
        }
      }
    }
  },calendarId, { conferenceDataVersion: 1 })

  Calendar.Events.remove(calendarId, response.id)

  if (response.conferenceData.createRequest.status.statusCode === "success") {
    const meetUrl = response.conferenceData.entryPoints[0].uri

    return meetUrl
  }

  return ''
}

const getTalkTheme = () => {
  return WADAI[Math.floor(Math.random() * (WADAI.length-1))]
}

const getTargetMembers = () => {
  return targetData.filter(data => data[header.indexOf('朝会参加')] === '◯')
}

const generateNeedInfo = arr => {
  return arr.map((names, index) => {
    return {
      members: names.map(name => `${name}さん`),
      eventLink: getMeetUrl() || DUMMY_LINKS[index] || 'https://meet.google.com/qsh-vfqu-ckb',
      talkTheme: getTalkTheme()
    }
  })
}

const generateMessageBlock = arr => {
  let blocks = [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "【:sparkles: Breaking Time :sparkles: ※11:00になりましたら、各リンクへご入室ください】\n・<https://note.com/overflow_inc/n/n812979ec5ef2|Breaking Time>とは\n・参加者の変更などは<https://docs.google.com/spreadsheets/d/hogehoge|こちら>から:woman-tipping-hand: \n\n雑談タイムで、10分間、社内メンバーと交流しましょう!\nトークテーマは、参考程度に!\nまた、参加者が2人以下の場合は、別のチームに飛び入り参加してください :+1: "
      }
    },
    {
      "type": "divider"
    }
  ]

  arr.forEach((a, index) => {
    blocks.push({
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": `*グループ${ALPHABETS[index]}*`
      }
    })
    blocks.push({
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": `:people_holding_hands: メンバー: ${a.members.join(", ")}\n\n:speech_balloon: トークテーマ:「${a.talkTheme}」\n\n:speech_balloon: RoomLink:<${a.eventLink}|こちらから入室>`
      }
    })
    blocks.push({
      "type": "divider"
    })
  })

  blocks.push({
    "type": "section",
    "text": {
      "type": "plain_text",
      "text": "今週も頑張っていきましょうー!"
    }
  })

  return blocks
}

const notificationToSlack = (messageBlock, room) => {
  const postUrl = `${自分で設定したSlackWebhookURL}`
  const roomName = room ? room : "#test_channel"
  const jsonData =
  {
    "channel": roomName,
    "blocks": messageBlock
  }
  const payload = JSON.stringify(jsonData)
  const options =
  {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload,
    "muteHttpExceptions": true
  }
  const res = UrlFetchApp.fetch(postUrl, options)
  return res.getContentText()
}
設定周り
Config.gs
const STAFF_LIST_SHEET_KEY = 'dummy'
const POST_TARGET_SLACK_CHANNEL = '#dummy'
const SHEET_NAME = 'dummy'
const DUMMY_LINKS = [
  'https://meet.google.com/dummy'
]
const ALPHABETS = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']

const targetBook = SpreadsheetApp.openById(STAFF_LIST_SHEET_KEY)
const targetSheet = targetBook.getSheetByName(SHEET_NAME)
const targetData = targetSheet.getDataRange().getValues()
const header = targetData[1]

const WADAI = [
  "最近気になっていること",
  "最近気になるニュース",
  "好きな芸能人",
  ....
]

エンジニア採用強化中

株式会社 overflow ではエンジニアメンバーを大募集中です。Offers の開発や overflow に興味を持たれた方ぜひご応募ください!中の雰囲気を知っていただくため、まずは副業からのスタートも歓迎しています。

https://offers.jp/jobs/2760
https://offers.jp/jobs/2711

とりあえず話を聞いてみたい!という方は当社 CTO とのカジュアル面談をお勧めしています。

https://meety.net/matches/PZIvsSzZrPXM

※上記の求人は本稿執筆時点の情報であり、閲覧時点で変更があった場合はご容赦ください