Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

学年はあるが学科は無い状態だけ「学科を追加」の導線が無かった

公開 読了時間 約4分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

学年が1つでもある学校はhasDepartments=falseのクラス制分岐に落ち、学科追加のUIが一切出ないため、既存の学年をそのまま学科制へ移行する手段が無かった。

結論

学校の構成管理画面はisEmptyhasDepartmentsという2つの真偽値で3状態に分岐していたが、そのうち「学年は1つ以上あるが学科は無い」状態(クラス制)だけ、学科を作る導線がどこにも存在しなかった。 新規校の空状態画面が用意していた「普通科(学科なし)で始める」というボタンを押すと、この学科を作れない状態へ一直線に入ってしまい、後戻りできなくなっていた。

症状

学校の階層構成(学科 → 学年 → クラス)を管理する画面には、学校の状態に応じて3種類の表示がある。

  • 学科も学年も無い新規校 → 「まだ学科・学年がありません」という案内と、最初の1件を作るボタン
  • 学科が1つ以上ある学校 → 学科ツリー表示+学科を追加するフォーム
  • 学科は無いが学年がある学校(クラス制) → 学年ツリー表示のみ

3つ目のクラス制の状態には、学科ツリー側にはある「学科を追加」フォームが存在しなかった。学科制の3階層管理(学科 → 学年 → クラス)に切り替えたい学校が、既存の学年を残したまま最初の1学科を作ろうとしても、画面上にその手段が無い。

さらに悪いことに、新規校向けの空状態画面には次の2つのボタンが並んでいた。

<button type="button" onClick={() => setShowDept(true)}>
  最初の学科を追加
</button>
<button
  type="button"
  onClick={() =>
    start(async () => {
      const res = await createGradeAction({ name: "1年" });
      report(res, "学年を追加しました(普通科)。");
    })
  }
>
  {pending ? "作成中…" : "普通科(学科なし)で始める"}
</button>

「普通科(学科なし)で始める」を押すとcreateGradeActionが呼ばれて学年が1つ作られる。この瞬間、学校は「学年はあるが学科は無い」クラス制の状態に切り替わり、学科を作る導線がある画面(空状態・学科ツリー)のどちらにも二度と戻れなくなる。

原因

画面の描画ロジックは次のように分岐していた。

const isEmpty = !hasDepartments && grades.length === 0 && otherLocations.length === 0;

{isEmpty ? (
  <EmptyState report={report} />
) : hasDepartments ? (
  <>
    {/* 学科ツリー */}
    <CollapsibleAdd label="学科を追加">
      <AddDepartmentForm report={report} />
    </CollapsibleAdd>
  </>
) : (
  <>
    {/* 学年ツリーのみ。学科を追加する手段が無い */}
    <CollapsibleAdd label="学年を追加">
      <AddGradeForm report={report} />
    </CollapsibleAdd>
    <p>学科制にすると「学科 → 学年 → クラス」の3階層で管理できます。</p>
  </>
)}

isEmptyhasDepartmentsの組み合わせで到達しうる状態は実質3つだが、「学科を追加する手段」を持っていたのはisEmpty(新規校)とhasDepartments(学科ツリー表示中)の2状態だけだった。残る1状態(!isEmpty && !hasDepartments=学年はあるが学科は無い)には、その手段が存在しないまま実装が進んでいた。

学科がゼロの学校を対象にした「学科へ移動」機能(OrphanBox)も、学科が1つ以上ないと表示されない部品のため、この状態からの迂回路にはならなかった。

直し方

クラス制の分岐にも、学科ツリー側と同じ「学科を追加」フォームを常設した。

<p style={hintStyle}>
  学科制にすると「学科 → 学年 → クラス」の3階層で管理できます。学科を追加すると、既存の学年は「学科へ移動」で各学科へ整理できます。
</p>
<CollapsibleAdd label="学科を追加">
  <AddDepartmentForm report={report} />
</CollapsibleAdd>

学科を追加した後は、それまで学校直下にあった既存の学年が「要整理」としてOrphanBoxに表示され、「学科へ移動」で各学科に振り分けられる。ヒント文にもその移行手順を補記した。

なぜ気づけなかったか

このコンポーネントの既存テストは、isEmpty状態とhasDepartments状態それぞれで学科・学年を追加できることを個別に検証していた。しかしisEmptyでもhasDepartmentsでもない3つ目の組み合わせ(学年はあるが学科は無い)で「学科を追加」できるかを検証するテストは存在しなかった。

画面が2つの真偽値の組み合わせで分岐している以上、本来なら4通り(実質はisEmptyhasDepartmentsを包含する形で3通り)すべての組み合わせを検証対象に挙げる必要があったが、そのうち「学科を追加する手段があるか」という観点では2状態しかテストされておらず、残る1状態がテストの空白のまま本番に出ていた。この状態に実際に到達する学校が現れるまで、空白は表面化しなかった。

よくある質問

Q1なぜクラス制の学校だけ学科を追加できなくなっていたのですか?

画面はisEmptyとhasDepartmentsという2つの真偽値で3状態に分岐しており、学科ツリーを表示中の学校と、学年もクラスも無い新規校の2状態には『学科を追加』の導線がありました。しかし学年はあるが学科は無い『クラス制』の状態だけ、その導線がどこにも存在しませんでした。

Q2どうやってその状態に入ってしまうのですか?

新規校の空状態画面には『普通科(学科なし)で始める』というボタンがあり、これを押すと学年が1つ作られて即座にクラス制状態に入ります。この時点で学科を作る手段は画面上から完全に消えます。

Q3OrphanBoxの『学科へ移動』機能で回避できなかったのですか?

できませんでした。OrphanBoxは学科が1つ以上存在する学校にのみ表示される部品で、学科がゼロの状態では出現しません。学校全体スコープの設置場所を持つ学校は、全削除してEmptyStateへ戻る迂回路も塞がれていました。

Q4なぜテストで見つからなかったのですか?

既存のテストはisEmpty状態とhasDepartments状態それぞれの学科追加導線を検証していましたが、学年はあるが学科は無いクラス制状態で学科を追加できるかを検証するテストは存在せず、この組み合わせだけ検証対象から漏れていました。

Q5直し方は何を変えたのですか?

クラス制分岐にも学科ツリー側と同じ、畳み式の『学科を追加』フォームを常設しました。追加後は既存の学年をOrphanBoxの『学科へ移動』で整理できるよう、ヒント文にも移行手順を補記しています。

確認した環境

  • Next.js ^16.0.0 / React ^19.0.0
  • 2026-07-25 に本番で発生・修正(PR #1342)

この記事の根拠

  • TypeScriptファイル 187〜239行目コミット b190aef
  • TypeScriptファイル 1352〜1382行目コミット b190aef
  • TypeScriptファイル 187〜247行目コミット e9c3b44

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。