記事一覧に戻る

AIで実装を加速しても、設計判断は省略できない —— シフト管理SaaSの改善記録

FlaskSQLAlchemyGoogle Calendar APIOAuth設計判断SaaSpytest

シフリーとは何か

シフリーは、学習塾や小規模店舗 (3〜30名規模) のシフト管理を効率化するWebアプリケーションです。

従来はLINEで希望を集め、Excelで調整し、確定したらGoogleカレンダーに手入力する —— という手作業の連鎖がありました。シフリーはこの流れをブラウザ上で完結させます。管理者がスケジュールを組み、オーナーが承認し、確定すると各スタッフのGoogleカレンダーにシフト予定が自動で書き込まれます。

管理者・ワーカー (スタッフ)・オーナー (経営者) の3者が、それぞれ異なる権限で操作するシステムです。Flask + Google Calendar API + PostgreSQLで構成し、Vercelにデプロイしています。

このシステムをAIコーディング支援を使って開発しました。API の雛形、データベースモデル、認証フロー、画面のDOM操作 —— これらは指示すればほぼ瞬時に出力されます。

問題はその先にありました。

カレンダー同期が壊れていた

シフリーの核心機能は「確定したシフトがワーカーのGoogleカレンダーに自動反映される」ことです。手入力を無くすことがこのシステムの存在意義です。

ところが、その自動反映が動いていませんでした。管理者がスケジュールを確定しても、ワーカーのカレンダーにはシフト予定が現れない。APIエラーが出ている。

最初の見え方は「Google Calendar APIの呼び出しが失敗している」でした。

実際の問題は違いました。

誰のカレンダーに、誰の認証情報で書き込むのか

Googleカレンダーに予定を書き込むには、そのカレンダーの持ち主から許可を得た認証情報が必要です。他人のカレンダーに勝手に書き込むことはできません。

シフリーでは、管理者がスケジュールを確定した時点で、各ワーカーのカレンダーにシフト予定を作成します。ここで選択が必要でした。

案A: 管理者の認証情報で、全員分のカレンダーに書き込む 一括処理できて実装は簡単ですが、管理者は他のワーカーのカレンダーへの書き込み権限を持ちません。APIは403 (権限なし) で拒否します。

案B: 各ワーカーの認証情報で、自分のカレンダーに書き込む ワーカーがログイン時にカレンダー書き込みを許可していれば動きます。ただし、許可が失効していたり、そもそもカレンダー権限を許可せずにログインしていた場合は失敗します。

「管理者が全員分をまとめて処理する」という一見便利な設計 (案A) を捨て、「各ワーカーが自分のカレンダーを自分の権限で管理する」設計 (案B) を選びました。

この選択の結果、同期が失敗したとき「田中さんのGoogleアカウントの権限が切れている」と特定できるようになりました。案Aのままでは「誰かの権限で全員分が失敗する」か「そもそも権限がなくて全員分が動かない」のどちらかです。

失敗を握りつぶさない

カレンダー同期が失敗したとき、最初の実装ではエラーを無視していました。プログラム上は try-except で例外を捕まえて何もせず次へ進む。アプリは止まりませんが、管理者からは「確定したはずなのにカレンダーに入っていない」と見えます。原因も対処もわかりません。

これをデータベースのレベルで改善しました。各ワーカーのシフト枠に「Googleカレンダー上のイベントID」と「最後に同期を試みた日時」を記録するようにしました。

  • イベントIDが空で、同期日時も空 → まだ同期していない
  • イベントIDが入っている → 同期成功
  • イベントIDが空で、同期日時が入っている → 同期を試みたが失敗した

管理者は管理画面から「どのワーカーの同期が止まっているか」を一目で把握し、そのワーカーにGoogleアカウントの再認証を依頼できます。

さらに、開校時間のカレンダー同期では、操作のたびに「何件成功し、何件失敗し、何件スキップしたか」をログとして記録しています。管理者が「昨日の同期は正常だったのか」を確認する手段を用意しました。

双方向同期で上書きを防ぐ

シフリーでは、塾の開校時間をGoogleカレンダーと双方向で同期できます。管理画面で「月曜14:00-21:00」と設定すれば、カレンダーに「開校時間」のイベントが自動で並びます。逆に、カレンダー上で臨時の開校時間を追加すれば、管理画面にもインポートされます。

ここで問題になるのは「どちら側の変更が正しいか」です。

管理画面で「12月25日は休校」と設定し、それをカレンダーにエクスポートする。次にカレンダーからインポートすると「12月25日は開校」のイベントがなかったのでエクスポートした情報が消える。次のエクスポートで再び「休校」を書き出す —— これでは無限ループです。

設計はこうしました。

  • カレンダーからインポートした予定には「カレンダーが出典」とマークする
  • 管理画面で手動作成した予定には「手動が出典」とマークする
  • エクスポートのとき、カレンダー出典の日はスキップする (カレンダー側が正なので上書きしない)
  • インポートのとき、手動出典の日はスキップする (管理者の判断を優先する)

出典の追跡がなければ、同期するたびにデータが行ったり来たりします。「どちらが情報源か」を各レコードに記録することで、この問題を根本から解決しました。

承認ワークフローの状態管理

シフトスケジュールは、管理者が作って終わりではありません。オーナー (経営者) の承認を経てから確定する必要があります。

流れはこうです。

管理者が作成 → オーナーに承認依頼 → オーナーが承認 (または差戻し) → 管理者が確定

「確定」を押すと、各ワーカーのGoogleカレンダーにシフト予定が自動作成されます。つまり、確定は取り消しの難しい操作です。

ここでの設計判断は「確定ボタンを2回押したらどうなるか」でした。

何も考えずに実装すると、2回押せば2回カレンダーにイベントが作られます。ワーカーのカレンダーに同じシフトが2件並ぶ。これは事故です。

対策として、確定操作を「冪等」にしました。すでに確定済みのスケジュールに対して確定を実行しても、何も起きません。確定は「承認済み」のスケジュールに対してのみ実行可能で、一度確定したら二度と確定できない一方通行の設計です。

また、すべての状態遷移 (承認依頼、承認、差戻し、確定) を2種類のログに記録しています。万が一障害が起きてデータの更新が途中で失敗しても、「何を試みて何が起きたか」の記録だけは残るようにしました。正常に動いているときよりも、壊れたときにこそ記録が必要です。

欠員補充の競合制御

ワーカーが急にシフトに入れなくなった場合、管理者は欠員を補充する必要があります。

シフリーでは、管理者が「この枠が空いた」と登録すると、その日に出勤可能な候補者にメールが届きます。メールには「承諾する」「辞退する」のリンクが入っており、候補者はワンクリックで応答できます。最初に承諾した人がそのシフトに入ります。

ここで問題になるのは「2人が同時に承諾した場合」です。

完全な排他制御 (一方が応答中はもう一方をブロックする) は実装が重いため、代わりにステータスチェックで対処しました。承諾処理の最初に「この欠員はまだ補充されていないか」を確認し、すでに誰かが承諾していたら「すでに補充済みです」と返します。完全ではありませんが、実用上十分に「先着1名」を保証できます。

応答リンクにはログインを要求しないステートレスなトークンを使っています。「承諾する」をタップするだけで完了します。ログインを要求すればセキュリティは上がりますが、シフト補充は「今日入れる人をすぐ見つけたい」場面です。即時性を優先しました。

AIはどこで効き、どこで効かなかったか

領域AIの貢献人間が判断したこと
APIエンドポイントの雛形高い
データベースモデルの定義高いテーブル間の関係、制約条件の選択
Google認証フローの実装高いどの権限をいつ要求するかの設計
カレンダー同期の責務設計低い誰の認証情報で誰のカレンダーに書くか
状態遷移の設計低い確定操作の冪等性、監査ログの設計
欠員補充の競合制御中程度セキュリティとUXのトレードオフ
テストコードの生成高い何をテストすべきかの優先順位

AIコーディング支援 (本プロジェクトではClaude Code) は、正しく動くコードを高速に生成します。しかし、「この場面でどちらの設計を選ぶか」は、人間が方針を決めなければ判断されません。

管理者fallbackを採用するかやめるか。同期の失敗を無視するかデータベースに記録するか。応答リンクにログインを要求するかしないか。

これらの判断は実装速度とは独立した時間を要します。むしろ、実装が速いぶんだけ「判断 → 実装 → 検証」のサイクルを何度も回せます。

改善を支えた仕組み

設計判断を安全に実行するための基盤も、開発と並行して整備しました。

  • pytest 237件: 管理者・ワーカー・オーナーの権限テスト、承認フロー、非同期タスク処理、テナント間のデータ分離、入力バリデーションをカバー。設計を変更するたびにテストを回し、改善が別の箇所を壊していないことを確認する
  • 監査ログ: 権限変更、承認、データ削除のすべてを記録。障害でデータ更新が失敗しても、「何を試みたか」の記録は残る設計にしている
  • トークンの暗号化: ワーカーのGoogleアカウント認証情報を暗号化して保存。万が一データベースが漏洩しても、認証情報はそのまま使えない

レビューで発見した改善対象

完璧ではありません。ソースコードの精読で見つかった、今後改善すべき点が3件あります。

  • タイムゾーンの不統一: カレンダー同期は日本時間 (Asia/Tokyo) を前提としているが、リマインダー送信は世界標準時 (UTC) で時刻を取得している。リマインダーが意図しない時刻に届く可能性がある
  • 欠員補充のカレンダー同期: 旧ワーカーのカレンダーイベントを削除するとき、カレンダーIDの指定方法に誤りがある。削除が常に失敗する
  • 非同期タスクのタイムアウト: メール送信やカレンダー同期を処理するバックグラウンドタスクにタイムアウト制限がない。処理が止まった場合の自動復旧手段がない

これらはいずれも、設計レビューの段階で拾えたはずの問題です。実装速度が上がった分、レビューの仕組みを開発の早い段階から意図的に組み込む必要がある —— これが開発を通じて得た実感です。

この事例が示すもの

AIでコードを生成する時代に、エンジニアの価値は「コードが書けること」ではなく「設計を決められること」に移りつつあります。

この記事で扱った判断 —— 認証責務の切り方、失敗の記録方法、双方向同期の出典管理、確定操作の安全設計、補充のセキュリティとUXのバランス —— はいずれも、AIに実装を指示する前に人間が方針を定めなければならないものでした。

実装は加速できます。しかし、壊れ方まで含めて設計することは省略できません。