🍟

会社の出勤退勤管理を少し楽にした話

に公開

日々のルーティン業務である出退勤記録を効率化する仕組みを作成しました。本記事では、Google Apps ScriptとDiscord Botを活用して、自動化を実現する手順をお話しします。

馴れ初め

私の職場では、以下のような手順で出退勤を記録しています:

  1. 出勤時
    • Discordに「おはようございます、出社してます」と投稿
    • スプレッドシートに出社時間を記入
  2. 退勤時
    • Discordに「お先に失礼します、お疲れ様でした」と投稿
    • スプレッドシートに退勤時間を記入

しかし、二度手間感があってめんどくさいと感じたので、これらを一元化し、自動化する仕組みを作りました。

作戦概要

以下の技術を活用して自動化を実現します:

  1. Google Apps Script
    • Googleスプレッドシートを操作するAPIを作成します。
  2. Discord Bot
    • Discordで特定のメッセージを受信したときに、Google Apps ScriptのAPIを呼び出します。

実装手順

1. Google Apps ScriptでスプレッドシートAPIを作成

ステップ1: スプレッドシートの準備

以下のレイアウトでスプレッドシート用意しました。

A列: No.
B列: 日付
C列: 曜日
D列: 出勤モード (“出勤” or “在宅”)
E列: 出勤時刻
F列: 退勤時刻
G列: 休憩時間

ステップ2: 新しいApps Scriptプロジェクトを作成

Google Apps Scriptを使って、出退勤時刻を受け取るお皿のようなものを作成します。

  1. スプレッドシートのメニューから 拡張機能 > Apps Script を選択

  2. 別タブでコードエディタが開くので、デフォルトの Code.gs を以下のように置き換えます。

// POSTリクエストから「出勤(oha1/oha2)」「退勤(otu)」を判別し
// スプレッドシートに時刻を書き込むメイン関数
function doPost(e) {
  try {
    // リクエストのJSONを解析
    const params = JSON.parse(e.postData.contents);
    const action = params.action; // 'oha1', 'oha2', 'otu' のいずれか

    // アクションごとに対応する関数を呼び出し
    if (action === 'oha1' || action === 'oha2') {
      // mode: '出勤' または '在宅'
      const mode = action === 'oha1' ? '出勤' : '在宅';
      recordCheckin(mode);
      return ContentService
        .createTextOutput(JSON.stringify({ status: 'success', message: `${mode}を記録しました。` }))
        .setMimeType(ContentService.MimeType.JSON);

    } else if (action === 'otu') {
      recordCheckout();
      return ContentService
        .createTextOutput(JSON.stringify({ status: 'success', message: '退勤を記録しました。' }))
        .setMimeType(ContentService.MimeType.JSON);

    } else {
      throw new Error('無効なアクションです: ' + action);
    }

  } catch (error) {
    // エラー時はJSONで返す
    return ContentService
      .createTextOutput(JSON.stringify({ status: 'error', message: error.message }))
      .setMimeType(ContentService.MimeType.JSON);
  }
}

ステップ3: 出勤/退勤を記録する関数を作る

// 出勤時刻を記入する関数
function recordCheckin(mode) {
  const sheet = getCurrentSheet();
  if (!sheet) throw new Error('シートが見つかりません');

  const todayStr = formatDate(new Date());
  const lastRow = sheet.getLastRow();

  for (let i = 2; i <= lastRow; i++) {
    const cellDate = sheet.getRange(i, 2).getValue();
    if (!(cellDate instanceof Date)) continue;
    if (formatDate(cellDate) !== todayStr) continue;

    // D列に mode、E列に時刻をセット
    sheet.getRange(i, 4).setValue(mode);
    sheet.getRange(i, 5).setValue(formatTime(new Date()));
    return;
  }
}

// 退勤時刻と休憩時間を記入する関数
function recordCheckout() {
  const sheet = getCurrentSheet();
  if (!sheet) throw new Error('シートが見つかりません');

  const todayStr = formatDate(new Date());
  const lastRow = sheet.getLastRow();

  for (let i = 2; i <= lastRow; i++) {
    const cellDate = sheet.getRange(i, 2).getValue();
    if (!(cellDate instanceof Date)) continue;
    if (formatDate(cellDate) !== todayStr) continue;

    // F列に退勤時刻、G列に休憩時間(固定1:00)をセット
    sheet.getRange(i, 6).setValue(formatTime(new Date()));
    sheet.getRange(i, 7).setValue('1:00');
    return;
  }
}

ステップ4: 補助関数を追加

// 現在の集計シートを選ぶ
function getCurrentSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const today = new Date();
  const year = today.getFullYear().toString().slice(-2); // 例: '24'
  const month = today.getMonth() + 1; // 1~12
  const day = today.getDate();

  // 16日以降なら翌月、1日~15日なら今月シートを参照
  const targetMonth = day > 15 ? month + 1 : month;
  const sheetName = `${year}${targetMonth}`;
  return ss.getSheetByName(sheetName);
}

// 日付を「M/d」形式の文字列に変換
function formatDate(date) {
  return Utilities.formatDate(date, Session.getScriptTimeZone(), 'M/d');
}

// 時刻を「H:mm」形式の文字列に変換
function formatTime(date) {
  return Utilities.formatDate(date, Session.getScriptTimeZone(), 'H:mm');
}

ステップ3〜5で以下のようなコードになるはずです
https://github.com/hitto-hub/DiscordWorkLogger/blob/main/code.gs

ステップ5: ウェブアプリとしてデプロイ

1.エディタ右上の デプロイ > 新しいデプロイをクリック
デプロイボタン

2.「種類」を ウェブアプリ に設定

- 説明:例)「勤怠記録API」

- 実行ユーザー:自分

- アクセス権限:**全員**、必要に応じて「自分のみ」または「組織内」に

3.「デプロイ」をクリック → 発行された URL をコピー

Done画面

発行されたURLは大切にメモしておきましょう

2. Discord Botの作成

Discord Botを使い、特定のメッセージに反応して先ほど作成した関数を呼び出す仕組みを作ります。

手順

DiscordBotを動かすにはサーバーが必要です。

私は自前のサーバーで動かしていますが、無料のサーバーを借りてDiscordBotを動かすこともできる(未検証)

コード

https://github.com/hitto-hub/DiscordWorkLogger/blob/main/main.py

  1. Botを作成

    • Discordの開発者ポータルで新しいBotを作成します。
      その際、Discord Bot のトークンをしっかりとメモしておきましょう

    参考

    https://discordpy.readthedocs.io/ja/stable/discord.html

  2. Botコードの実装

メッセージをトリガーにGoogle Apps Script APIを呼び出すDiscordBotを実装します。

  • .env ファイルを作成

.env.exampleを参考に適当に設定して下さい

```env
DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN
TARGET_GUILD_ID=YOUR_GUILD_ID
TARGET_CHANNEL_ID=YOUR_CHANNEL_ID
```

DISCORD_TOKEN

しっかりとメモしておいたトークン

TARGET_GUILD_ID

TARGET_CHANNEL_ID(お疲れ様でしたと報告するチャンネル)

  • ユーザー ID と GAS URL のマッピング

user_gas_mapping.json ファイルを作成し、以下のようにユーザー ID と GAS の Web App URL をマッピングします:

左側にDiscordのユーザーID, 右側に大切にメモしておいたgoogle apps scriptのURLを書いていく

{
  "123456789012345678": "https://script.google.com/macros/s/abc1234567890/exec",
  "876543210987654321": "https://script.google.com/macros/s/def0987654321/exec"
}
  • 起動させる
DockerでDiscord Bot の起動
  1. Docker イメージをビルド

    docker compose build
    
  2. Docker コンテナを起動

     docker compose up -d
    
Discord Bot の起動
  1. Python スクリプトを実行

    python main.py
    

結果

実際に動かすと、以下のような流れで自動記録が行われます。

1.出勤時の挙動

  • Discord で おはようございます、出社してます と投稿

  • Bot が反応してリアクションで反応

  • 当日該当行の D 列に 出勤、E 列に HH:mm(例: 9:05)が自動入力される

2.在宅出勤時の挙動

  • Discord で おはようございます、始めます と投稿

  • Bot が反応してリアクションで反応

  • 当日該当行の D 列に 在宅、E 列に HH:mm が自動入力される

3.退勤時の挙動

  • Discord で お先に失礼します、お疲れ様でした と投稿

  • Bot が反応してリアクションで反応

  • 当日該当行の F 列に HH:mm(例: 18:30)、G 列に休憩時間 1:00 が自動入力される

実際に仕組みを導入すると、以下のような効果を得られました:

  • 手作業の削減で業務効率アップ
  • 記録漏れの防止
  • 幸せになれる

感想

この仕組みを作成してみて、一番感じたのは「思っていたよりも簡単に実現できた!」ということです。Google Apps ScriptやDiscord Botは過去に触ったことがあったので2時間くらいであまり苦戦せずに作成できました。

また、200行程度のコードで日々の業務を効率化できると考えると、プログラミングの力は偉大だなと恐怖すら覚えました。


最後に、

今回の取り組みで、普段の業務を少しだけ楽にすることができました。同じように感じている方はぜひ試してみてください!

詳細なコードはこちらのGitHubにアップしています:https://github.com/hitto-hub/DiscordWorkLogger

おまけ

この記事の存在意義あるか?

Discussion