Rebounder Tech Blog

Written by the people who actually run these systems in production.

Only One Branch Had No Way to Add the First Item

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

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

結論

A school with even one grade falls into the hasDepartments=false class-system branch, where no department-adding UI appears at all, leaving no way to move existing grades to a department system.

The short version

The school structure management screen branches three ways on two booleans, isEmpty and hasDepartments, and one of those states — “one or more grades but no departments” (the class system) — had no way anywhere to create a department. Pressing the button the new-school empty state offered, 「普通科(学科なし)で始める」 (start with a general course, no departments), took you straight into that state with no way back.

What it looks like

The screen managing a school’s hierarchy (department → grade → class) has three displays depending on the school’s state.

  • A new school with neither departments nor grades → a note saying none exist yet, and buttons to create the first one
  • A school with one or more departments → the department tree plus a form to add a department
  • A school with grades but no departments (the class system) → the grade tree only

The third, class-system state had no “add department” form of the kind the department-tree side has. A school wanting to switch to three-level management by department (department → grade → class) had no means on screen of creating its first department while keeping its existing grades.

Worse, the empty state for a new school offered these two buttons side by side.

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

Pressing 「普通科(学科なし)で始める」 calls createGradeAction and creates one grade. At that instant the school switches to the class-system state with grades but no departments, and can never return to either screen that has a way to create a department (the empty state or the department tree).

Why

The rendering logic branched like this.

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

{isEmpty ? (
  <EmptyState report={report} />
) : hasDepartments ? (
  <>
    {/* department tree */}
    <CollapsibleAdd label="学科を追加">
      <AddDepartmentForm report={report} />
    </CollapsibleAdd>
  </>
) : (
  <>
    {/* grade tree only. No means of adding a department */}
    <CollapsibleAdd label="学年を追加">
      <AddGradeForm report={report} />
    </CollapsibleAdd>
    <p>学科制にすると「学科 → 学年 → クラス」の3階層で管理できます。</p>
  </>
)}

The combinations of isEmpty and hasDepartments reach effectively three states, and only two of them — isEmpty (a new school) and hasDepartments (showing the department tree) — carried a means of adding a department. The remaining state (!isEmpty && !hasDepartments, grades but no departments) was implemented without one.

The “move to department” feature (OrphanBox) aimed at schools with zero departments was also no detour out of this state, being a component that only appears once at least one department exists.

Fixing it

We gave the class-system branch the same permanent “add department” form as the department-tree side.

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

Once a department is added, the existing grades that had sat directly under the school appear in OrphanBox as needing organisation, and can be distributed to departments with “move to department”. The hint text gained those migration steps too.

Why it wasn’t noticed

This component’s existing tests verified individually that departments and grades can be added in the isEmpty state and in the hasDepartments state. But there was no test asking whether a department can be added in the third combination, neither isEmpty nor hasDepartments (grades but no departments).

Given a screen branching on the combination of two booleans, all four combinations (effectively three, since isEmpty subsumes hasDepartments) should have been listed for verification. On the question “is there a means of adding a department”, only two states were tested, and the remaining one went to production as a blank in the tests. The blank did not surface until a school actually reached that state.

よくある質問

Q1Why could only class-system schools not add a department?

The screen branches three ways on two booleans, isEmpty and hasDepartments. Both the school showing a department tree and the brand-new school with no grades or classes had an "add department" entry point. Only the class-system state — grades but no departments — had it nowhere.

Q2How do you end up in that state?

The empty state for a new school has a button reading 「普通科(学科なし)で始める」 (start with a general course, no departments). Pressing it creates one grade and drops you straight into the class-system state, where every way to create a department disappears from the screen.

Q3Couldn't OrphanBox's "move to department" work around it?

It could not. OrphanBox is a component shown only to schools with at least one department, and it does not appear when there are zero. Schools with a school-wide location also had the detour of deleting everything to return to EmptyState closed off.

Q4Why did the tests not find it?

Existing tests verified the department-adding entry point in the isEmpty state and in the hasDepartments state, but there was no test asking whether a department can be added in the class-system state — grades but no departments — so that combination alone was out of scope.

Q5What did the fix change?

We made the class-system branch carry the same collapsible "add department" form as the department-tree side. The hint text also gained the migration steps, so existing grades can be organised afterwards with OrphanBox's "move to department".

確認した環境

  • Next.js ^16.0.0 / React ^19.0.0
  • Occurred and fixed in production on 2026-07-25 (PR #1342)

この記事の根拠

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

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