教室スケジュールをスワイプで
週を切り替えられるようにした

関連記事

1. 公開ページを4週カルーセルにした

教室スケジュールをしばらく運用していると、いくつか課題に気づきました。

まずは、週の予定の切替方法です。
以前は1週ずつ切り替える形で、先の週を確認するときには、画面遷移が必要でした。

そこで4週分のデータを最初にすべて DOM に持たせておき、上部ボタンとスワイプの両方で切り替えられる形にしました。

$weeks[] = [
    'index' => $w,
    'label' => $weekLabels[$w],
    'dates' => $dates,
    'data'  => $data[$weekKey] ?? getDefaultWeekData(),
    'display_schedule' => buildPublicDisplaySchedule(
        $data[$weekKey] ?? getDefaultWeekData(),
        $dayKeys,
        count($timeSlots)
    ),
];Code language: PHP (php)

変更のポイントは、週切替をサーバーの再描画ではなくクライアント側の処理に寄せたことです。
PHP 側は4週分のデータを組み立て、JavaScript では translate3d の値を切り替える実装です1

function setActiveWeek(nextIndex, withAnimation) {
    activeIndex = Math.max(0, Math.min(buttons.length - 1, nextIndex));
    track.style.transition = withAnimation ? 'transform 0.28s ease' : 'none';
    track.style.transform = 'translate3d(-' + (activeIndex * 100) + '%, 0, 0)';
}Code language: JavaScript (javascript)

4週分を一度に DOM に持つので遅延描画はしていません。
4週を超える要件が出た場合、同じ構造のまま増やすとやや重くなります。

2. Google 同期の除外プレフィックスを最優先にした

Google同期に × といった記号で始まるカレンダー予定をコメント扱いにする仕組みを追加しました。
スマートフォンから登録するときは、「ばつ」にはいくつか記号があるので、どれも許容できるように増やしました。

また、「授業」や「休講」などのキーワードの判定と優先順も整理しました。
除外プレフィックスをすべての判定より先に評価する順序に固定しました。
たとえば ✗終日休校 というタイトルは、単語だけ見れば「休校」が含まれますが、先頭記号があるので無視されます。

優先順の崩壊はサイレントに起きるので、テストで境界を固定しておくことが大事です2

if (isLessonSummary('✗授業')) {
    fwrite(STDERR, "Test failed: excluded prefix should override 授業 match\n");
    exit(1);
}Code language: PHP (php)

行頭の空白とプレフィックスの組み合わせも明示的に確認しています。

if (isLessonSummary(" \t×授業")) {
    fwrite(STDERR, "Test failed: leading whitespace + excluded prefix should be ignored\n");
    exit(1);
}Code language: PHP (php)

ルールを増やすより「何を先に除外するか」をテストで固定した方が、あとから壊れにくいです。

3. 「今週」の境界を金曜 15:10 に統一した

あとは、週明けの予約をしやすくすることです。

週末には、授業が終わった週のスケジュールを表示しても使いにくいです。
金曜④限の終了後、つまり 15:10:00 までは当週、15:10:01 以降は翌週として共通の関数に寄せました。

テストは境界値を直接持っています3

$beforeBoundary = new DateTimeImmutable('2026-04-24 15:10:00', new DateTimeZone('Asia/Tokyo'));
$afterBoundary  = new DateTimeImmutable('2026-04-24 15:10:01', new DateTimeZone('Asia/Tokyo'));

assertSameValue(
    appCurrentWeekMonday($beforeBoundary)->format('Y-m-d'),
    '2026-04-20',
    'friday end time itself should still belong to the current week'
);

assertSameValue(
    appCurrentWeekMonday($afterBoundary)->format('Y-m-d'),
    '2026-04-27',
    'after friday final slot ends, current week should advance'
);Code language: PHP (php)

ここで DateTime ではなく DateTimeImmutable を使っているのは意図的で、テスト用の値が意図せず書き換わる事故を防ぐためです4
タイムゾーンは Asia/Tokyo 固定なので、別 TZ で動かす場合はそのまま使えません。

4. 設計として守ったこと

今回の変更全体を通じて共通していたのは、レイヤーを混ぜないという方針でした。

表示専用の処理は表示専用のまま閉じ込める。
週境界や同期判定のようなルールはテストで固定する。

これらは個別の設計判断というより、小さく保ちながら動き続けるシステムのための一貫した姿勢だと思っています。

次に触るときの確認ポイントをメモとして残しておきます。

  • 公開ページを変更するときは index.php だけでなく styles.csslib/public_schedule_display.php も確認する
  • 同期ルールを変えるときは除外プレフィックスが最優先のままかを tests/google_calendar_sync_test.php で確認する
  • 週境界を変えるときは tests/current_week_boundary_test.php を先に直してから実装を動かす
  • デプロイ対象を変えたら README.md も同時に更新する
  1. translate3d を使うと、ブラウザはその要素を独立したコンポジットレイヤーに昇格させ、アニメーション処理を GPU に委ねます。これにより、週切替のスライドが CPU の再レイアウト処理を経ずに描画されます。ただしレイヤーが増えすぎると VRAM を消費するため、モバイルでは枚数に注意が必要です。 – Aerotwist – On translate3d and layer creation hacks
  2. 判定ルールが複数あるとき、優先順を文書化するだけでは不十分で、テストコードで「何が先に勝つか」を固定しておくことが有効です。実装を変えたとき、テストが赤くなれば優先順が崩れたとすぐわかります。ルール数が増えるほどこの効果は大きくなります。
  3. 境界値テストでは、境界の「ちょうど」と「1単位ずれた値」の両方を確認するのが基本です。今回は秒が境界単位なので 15:10:0015:10:01 を別ケースとして持ちます。どちらか片方だけだと、境界の片側が検証されないまま実装が変わったとき気づけません。
  4. PHP の DateTime はメソッド呼び出しでオブジェクト自身が変化するミュータブルな設計になっています。一方 DateTimeImmutable は変更操作のたびに新しいオブジェクトを返し、元の値は変わりません。テストコードで複数の比較を同じ値から派生させる場合、DateTime を使うと意図しない副作用が起きやすいです。 – PHP: DateTimeImmutable – Manual