0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【勤怠入力ゼロ】Dropboxの作業ログから「出勤・退勤」を自動生成する実験(GAS×差分カーソル×毎日00:05)

Last updated at Posted at 2026-01-28

※前回記事の続編です:
〖建築士×ChatGPT〗「読み方も分からない技術」で、野良DXFを3秒で解析する爆速ツールをHTML1枚で作った話


🚀 先に結論(TL;DR)

「勤怠入力」を社員にさせたくないので、Dropboxの作業ログから出勤・退勤(+休憩)を自動推定する仕組みを作っています。

  • GASで毎日 00:05(JST) に自動実行
  • Dropboxの更新ログから、担当者別に 出勤簿(1日1行)+月次集計(1人1行) を自動生成
  • 休憩判定は、建築実務に合わせた 「可変残像時間(Variable Cooldown)」 を実験中
  • 今回の改訂ポイント:CADの一定間隔の自動保存を織り込み、自動保存だけで勤務が延々と伸びる誤判定を防ぐ
  • 利用しているDropbox:Dropbox Business(Standard)

🤔 なぜ「勤怠入力」をやめたいのか(うちの事情)

私の運営する設計事務所は、少し変わった働き方をしています。

  • 全社員フルリモート(出社義務なし)
  • 定時なし(働いた時間=成果ベースの給与)
  • 多様な働き方(育児・家事・介護と並走しながら働く社員もいる)

この前提で一般的な「打刻」や「日報」を求めると、現場はこうなります。

  1. 作業が細切れになり、入力回数が増えて地獄
  2. 仕事に集中したいあまり、書き忘れが多発
  3. 月末に記憶を辿って修正する「虚無の時間」が発生

何より、 「業務以外の作業を社員にさせる時間がもったいない」 のです。
その1分があるなら、素晴らしい建築を考える時間に充ててほしい。

そこで発想を逆転しました。普段の業務ログ(Dropbox)から“勝手に勤怠ができる” を目指しています。


🛠️ いま出来ていること(MVP)

以下の仕様ですでに運用を開始しています。

  • 毎日 00:05(JST)に自動実行
  • 直近24時間のDropbox更新を見て、担当者ごとに:
    • 最初の作業時刻=出勤
    • 最後の作業時刻=退勤
  • 出勤簿: 出勤簿_担当者名 シートに1日1行(重複は自動整形でマージ)
  • 月次集計: 月次集計 シートに「担当者×年月」で1人1行
  • 監査ログ: 監査ログ は毎日上書き(肥大化防止)

🎯 将来のゴール:時給まで“自動で”算出したい(休憩が最後のピース)

弊社は従業員ごとの 売上・利益 を算出しています。
ここに 勤務時間が正確に紐づくと、個人ごとの 実質時給(利益ベースの時給) を算出できます。

将来的には、こういう状態を目指しています。

  • 従業員は「勤怠」や「時間」を意識せず、仕事に集中する
  • 裏側で勤務時間が自動集計され、時給(評価指標)が自動で変動する
  • 経営側は感覚ではなく、数字で改善できる(負荷・収益性・配置の最適化)

実は、売上・利益側のデータは現時点で揃っていて、足りないのは「休憩」だけです。
つまり 休憩時間が最後のピース。だからこそ、ここを最優先で詰めています。


🧪 挑戦:建築士の「休憩」をどう判定するか(改訂:自動保存込み)

このシステムの最大の課題は、 「ログが途切れている時間が、休憩(中抜け)なのか、思考中なのか」 の判別です。

一般的なツールなら「PC操作が〇分止まったら休憩」としますが、建築士の業務はそう単純ではありません。
図面を見ながら2時間悩み続けることもあれば、事務処理をパパッと終わらせて子供の迎えに行くこともあります。

さらに弊社の場合、CADソフトが 一定時間ごとに自動保存する設定になっています。
自動保存データは 通常の保存と別形式で判別可能です。

ただし「ログがある=勤務中」と単純化すると、次の誤判定が起きます。

  • 席を外していても自動保存だけ動く
  • その結果、勤務が延々と伸びる(=休憩が消える)

そこで現在は、次の2つを“別々の話”としてではなく、一つの理論として統合した休憩推定アルゴリズムを設計しています。


🧠 理論:自動保存込み「可変残像時間(Variable Cooldown)」統合理論

本理論の核は、ログを 2種類(強ログ/弱ログ) に分けて扱うことです。

  • 強ログ(Strong):人の操作が確実な更新
    例)通常保存、明示的な書き出し、手動での編集保存など
    勤務ブロックの開始・延長の根拠にする(可変残像で“思考時間”を救う)

  • 弱ログ(Weak):CAD自動保存など、人の操作と一致しない可能性がある更新
    例)一定間隔のAuto Save、バックアップ、テンポラリ保存など
    勤務ブロックは開始させない延命は最小限に制限(“延々伸びる”を防ぐ)

1) 強ログに対する「可変残像(Variable Cooldown)」

最後に触ったファイルの拡張子に応じて、その後の「作業とみなす時間(残像)」を動的に変化させます。

  • CAD/BIMデータ(.dwg, .rvt, .pln など)
    • 特性: 修正しては考え、また修正する。画面を見ながら考える時間が長い。
    • 判定: 更新後 120分 はログがなくても勤務中(思考中)とみなす。
  • 事務・画像データ(.pdf, .xlsx, .jpg など)
    • 特性: まとめて処理することが多い。思考時間は比較的短い。
    • 判定: 更新後 30分 ログがなければ休憩(離席)とみなす。

2) 弱ログ(自動保存)に対する「延命ルール」

自動保存は便利ですが、勤怠推定に入れると誤判定の温床になります。
そこで次の制約を設けます。

  • 弱ログは それ単体では勤務ブロックを開始しない
  • 弱ログは すでに勤務ブロック中の場合のみ、短時間だけ延長を許可(例:最大10分)
  • さらに安全策として、強ログ由来の期限(Strong End)を超えて延命しない
    → 自動保存が夜中に出続けても、勤務は伸びない

🔎 処理イメージ(統合理論の動き)

時系列のログを上から順に走査し、各イベントが持つ「有効期限(タイムスタンプ+残像)」を更新していきます。
ポイントは、強ログは“可変残像で期限を作る”弱ログは“ブロック中だけ短く延命する(上限あり)” という役割分担です。

時刻 ファイル 種別 残像/延命 判定(有効期限)
09:00 図面A.dwg +120分 11:00まで勤務中
09:10 図面A_autosave.bak +10分(ただし強期限を超えない) 11:00のまま(延びない or 最小限)
10:30 見積書.xlsx +30分 11:00(強期限の方が長いので維持)
12:00 (ログなし) - - 期限切れ → 休憩開始
13:00 図面B.dwg +120分 作業再開(12:00-13:00は休憩)

このアルゴリズムにより、「図面を描いていて手が止まっていた時間」は勤務として救済され、「事務処理が終わって子供の迎えに行った時間」は休憩として正確に除外されるようになりました。

この統合理論により、 「思考時間は救う」と「自動保存では延々伸ばさない」 を同時に実現できます。


🧩 自動保存の判別方法(実装の考え方)

自動保存は「別形式で判別可能」なので、実装では次のいずれか(または複合)で判定します。

  • 拡張子(例):bak / tmp / autosave
  • ファイル名パターン(例):~$ / .sv$ / .autosave
  • 専用フォルダ配下(例:/CAD_AUTOSAVE/ のようにパスで判定)

✍️ 実装コード(統合理論の関数:強ログ+弱ログ+可変残像)

/**
 * 自動保存を考慮した可変残像時間アルゴリズム(統合版/実験)
 * - 強ログ(通常保存/編集など)でブロック開始・延長(可変残像)
 * - 弱ログ(自動保存)は「ブロック中のみ」短時間延長(延命のみ)
 * - 弱ログだけでブロック開始しない/強ログ期限を超えて延命しない
 *
 * @param {Array} logs - 時系列順 [{time: Date, ext: string, name?: string, path?: string}, ...]
 * @return {number} 実働時間(時間)
 */
function calculateVariableWorkHoursV2(logs) {
  if (!logs || logs.length === 0) return 0;

  // 1) 強ログ(可変残像):拡張子ごとの勤務扱い延長(分)
  const COOLDOWN_STRONG_MIN = {
    // CAD・BIM系:長く考える
    'dwg': 120, 'dxf': 120, 'pln': 120, 'rvt': 120,
    // 事務・画像系:短め
    'xls': 30, 'xlsx': 30, 'pdf': 30, 'jpg': 30, 'jpeg': 30, 'png': 30,
    // その他
    'default': 45
  };

  // 2) 弱ログ(自動保存):延命は短め固定+上限(分)
  const AUTOSAVE_EXTS = new Set(['bak', 'tmp', 'autosave']); // ←会社の実態に合わせて追加
  const AUTOSAVE_NAME_PATTERNS = [
    /^~\$/,      // 例:~$xxxx
    /\.sv\$/i,   // 例:xxxx.sv$
    /\.autosave/i
  ];
  const AUTOSAVE_COOLDOWN_MIN = 10; // 弱ログで延命してよい最大(分)

  function isAutoSave(log) {
    const ext = (log.ext || '').toLowerCase();
    const name = (log.name || '');
    if (AUTOSAVE_EXTS.has(ext)) return true;
    for (const re of AUTOSAVE_NAME_PATTERNS) {
      if (re.test(name)) return true;
    }
    // pathで判定したい場合(例:/CAD_AUTOSAVE/ 配下など)
    // if ((log.path || '').includes('/CAD_AUTOSAVE/')) return true;
    return false;
  }

  function strongCooldownMs(extLower) {
    const min = COOLDOWN_STRONG_MIN[extLower] || COOLDOWN_STRONG_MIN['default'];
    return min * 60 * 1000;
  }

  const autosaveCooldownMs = AUTOSAVE_COOLDOWN_MIN * 60 * 1000;

  let totalWorkMs = 0;

  // 勤務ブロック
  let currentBlockStart = null;
  let currentBlockEnd = null;

  // 強ログが最後に出たときの期限(弱ログだけで延命し続けないため)
  let lastStrongEnd = null;

  for (let i = 0; i < logs.length; i++) {
    const t = logs[i].time.getTime();
    const ext = (logs[i].ext || '').toLowerCase();
    const auto = isAutoSave(logs[i]);

    // まだ勤務ブロックが無い
    if (currentBlockStart === null) {
      // 弱ログだけでは勤務開始しない
      if (auto) continue;

      // 強ログなら開始
      currentBlockStart = t;
      currentBlockEnd = t + strongCooldownMs(ext);
      lastStrongEnd = currentBlockEnd;
      continue;
    }

    // ブロック外に出ている(ギャップ)
    if (t > currentBlockEnd) {
      totalWorkMs += (currentBlockEnd - currentBlockStart);

      // 次のブロック:弱ログでは開始しない
      if (auto) {
        currentBlockStart = null;
        currentBlockEnd = null;
        lastStrongEnd = null;
        continue;
      }

      // 強ログなら新ブロック開始
      currentBlockStart = t;
      currentBlockEnd = t + strongCooldownMs(ext);
      lastStrongEnd = currentBlockEnd;
      continue;
    }

    // ブロック継続中
    if (!auto) {
      // 強ログ:通常通り延長(長い方を採用)
      const endCandidate = t + strongCooldownMs(ext);
      currentBlockEnd = Math.max(currentBlockEnd, endCandidate);
      lastStrongEnd = Math.max(lastStrongEnd, endCandidate);
    } else {
      // 弱ログ(自動保存):
      // - ブロック中のみ短時間延長を許可
      // - ただし強ログ期限(lastStrongEnd)を超えて延命しない
      if (lastStrongEnd == null) continue;

      const endCandidate = t + autosaveCooldownMs;
      const cappedEnd = Math.min(endCandidate, lastStrongEnd);
      currentBlockEnd = Math.max(currentBlockEnd, cappedEnd);
    }
  }

  // 最後のブロックを加算
  if (currentBlockStart !== null) {
    totalWorkMs += (currentBlockEnd - currentBlockStart);
  }

  return totalWorkMs / (1000 * 60 * 60); // 時間単位で返す
}

この統合理論により、 「思考時間は救う」と「自動保存では延々伸ばさない」 を同時に実現できます。


💡 技術的な壁と解決策(開発ストーリー)

エンジニアではない私が、ChatGPTと壁打ちをして辿り着いた「試行錯誤の記録」です。

❌ 失敗:監査ログ(Team Log API)は使い物にならない

最初は「会社の管理機能なんだから、監査ログ機能を使えばいい」と思っていました。
しかし、Dropboxの team_log/get_events を試すと、以下の壁にぶつかりました。

  • ノイズが多すぎる: 「デバイス同期」「プレビュー」などが大量に混ざる。
  • 情報が欠ける: ファイル名やパスが空(null)で返ってくることがあり、業務特定できない。

⭕️ 成功:フォルダ差分(Cursor)方式への転換

そこで目をつけたのが、files/list_folder APIの Cursor(カーソル)機能 です。
これはGitのコミットログのように、「前回確認した時点からの差分」だけを教えてくれる機能です。

  • 初回: get_latest_cursor で「今の状態」をセーブポイントとして保存。
  • 次回: そのセーブポイントを渡すと、「そこから増えた・変わったファイルだけ」 を返してくれる。

これにより、何万ファイルある事務所でも、GASの「6分の壁(実行時間制限)」に引っかかることなく、数秒で全社員の生存確認が終わるようになりました。


🏁 まとめ:DXの目的は「楽をするため」

建築設計事務所におけるDXの目的は、高価なSaaSを入れることでも、かっこいいツールを自慢することでもありません。
「人間がやらなくてもいいことを、徹底的に機械にやらせる」 ことに尽きます。

「勤怠入力」という、一銭の利益も生まない作業をゼロにする。
そのために技術を使う。
浮いた時間で、社員は子供と遊んだり、新しいデザインを考えたりできます。

そして将来的には、休憩推定まで含めて勤務時間が正確に揃うことで、従業員ごとの時給(利益ベース)まで自動で算出し、現場は時間を気にせず仕事に集中できる仕組みに育てたいと考えています。

この実験が、同じ悩みを持つ中小企業の経営者や、バックオフィスの担当者のヒントになれば幸いです。


免責

本記事は「業務効率化の実験」の記録です。
労務管理や就業規則との整合性は会社ごとに異なりますので、最終的な運用は顧問社労士等とご相談の上で導入してください。

👉 【建築士×GIS】Google Maps上で「雨水の流下経路」を1クリック追跡する“HTML1枚”ツールを作った(国土地理院 標高×道路中心線×Cloudflare Pages)


参考(一次情報)

  • Dropbox OAuth Guide (offline access / refresh token)
  • Dropbox: Detecting Changes Guide (cursor差分設計)
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?