「自由テキスト入力で作ったカテゴリは、3日後に必ず表記ゆれで崩壊する」
令和に自作した2ちゃんねる風掲示板「つゔぁいちゃんねる(2ch.biz)」開発秘話の第5弾。今回は、板が増えるにつれて直面した「カテゴリの分裂・表記ゆれ問題」を解決し、美しい階層構造を取り戻すためのデータ設計と管理画面UIの刷新について解説します。
💡 はじめに:2ちゃんねるにおける「カテゴリ」の役割
2ちゃんねるを開いたとき、左側にずらりと並ぶ板一覧(サイドバー)を見たことがあるでしょう。
- 地震・ニュース : 地震速報、ニュース速報、ビジネスnews+
- ネット関係 : ネットカフェ、Web制作、プログラマー
- 文化・サブカル : アニメ、漫画、同人、ゲーム
この「カテゴリ」という大分類は、単なるWebサイトのタグではありません。 何百・何千という板が乱立する広大なネット空間において、ユーザーが目的の広場に辿り着くための唯一の羅針盤 です。
しかし、つゔぁいちゃんねるの超初期の実装では、このカテゴリの扱いを甘く見ていました。
💥 初期実装の失敗:自由テキスト入力が引き起こした「カテゴリ分裂」
最初に板作成機能を実装した際、私は深く考えずに板作成フォームのカテゴリ欄を <input type="text">(自由テキスト入力) にしていました。
これが大失敗の始まりでした。
管理者が板を追加していくうちに、以下のような地獄の表記ゆれが発生したのです。
- ある板は
ニュース - 別の板は
NEWS - さらに別の板は
ニュース系 - タイポで
ニース
トップページや専ブラのメニュー画面では、これらがすべて「別の独立したカテゴリ」としてバラバラに認識され、 同じジャンルの板が4つの異なる見出しに分裂して表示される という無惨な光景が広がりました。
「これは人間が手入力する仕組みにしていてはダメだ。カテゴリを独立した第一級オブジェクトとしてデータベースで管理しなければならない」と痛感し、リレーショナル設計への刷新を決意しました。
🏗️ データベース設計の刷新:独立した categories テーブルの導入
板(boards)テーブルの文字列カラムに依存するのをやめ、独立した categories テーブルを新設しました。
-- カテゴリテーブルの新設
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- boards テーブルとの論理的連携(または category_id 外部キー)
-- つゔぁいちゃんねるでは後方互換性と移行コストを考慮し、
-- 連動トリガー・トランザクションによる名前同期を採用
なぜ完全な外部キー(ID)にしなかったのか?
レトロな専ブラ用プロトコル(bbsmenu.html)や既存のAPIレスポンスでは、各板のカテゴリが「文字列の名前」として埋め込まれている箇所が多数ありました。
コードベース全体を一撃で壊さないよう、 「カテゴリマスターテーブルで一元管理しつつ、カテゴリ名変更時は所属する板のカテゴリ文字列を一括トランザクションで同期更新する」 という実践的な移行アプローチを取りました。
🔄 カテゴリ名変更時のカスケード同期トランザクション
管理画面から「カテゴリ名」を変更した際、もし1つの板でも古い名前のまま取り残されたら不整合が起きます。
そのため、サーバーサイド(api.php)では必ずDBトランザクションを張り、アトミックに同期させます。
public function updateCategory($categoryId, $newName) {
$newName = trim($newName);
if (empty($newName)) {
throw new InvalidArgumentException("カテゴリ名は必須です。");
}
$this->db->beginTransaction();
try {
// 1. 旧カテゴリ名を取得
$stmt = $this->db->prepare("SELECT name FROM categories WHERE id = :id");
$stmt->execute([':id' => $categoryId]);
$oldName = $stmt->fetchColumn();
if (!$oldName) {
throw new RuntimeException("該当するカテゴリが存在しません。");
}
// 2. categories テーブルの名称更新
$stmt = $this->db->prepare("
UPDATE categories
SET name = :name, updated_at = NOW()
WHERE id = :id
");
$stmt->execute([':name' => $newName, ':id' => $categoryId]);
// 3. 所属するすべての板の category カラムを連動して一括更新
$stmt = $this->db->prepare("
UPDATE boards
SET category = :newName
WHERE category = :oldName
");
$stmt->execute([':newName' => $newName, ':oldName' => $oldName]);
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
この処理のおかげで、例えば「IT」というカテゴリを後から「テクノロジー&AI」に変更した場合でも、所属する「Web制作板」「プログラミング板」「AI板」などが一瞬で新しいカテゴリ配下に整合性を保って移動します。
🎨 UIの進化:ドロップダウン化と削除時の安全ガード
管理画面のUIも根本から見直しました。
1. 板作成フォームの <select> ドロップダウン化
自由入力を廃止し、登録されているカテゴリからしか選べないように制限。
<!-- 板作成モーダル内のカテゴリ選択 -->
<div class="form-group">
<label for="boardCategorySelect">所属カテゴリ <span class="required">*</span></label>
<select id="boardCategorySelect" class="form-control" required>
<!-- admin.js が categories テーブルから動的にオプションを生成 -->
<option value="">-- カテゴリを選択してください --</option>
</select>
</div>
2. 所属板があるカテゴリの誤削除防止
板が1つでも紐付いているカテゴリを誤って削除してしまうと、その板がトップページで迷子(孤児)になってしまいます。
カテゴリ削除時には、必ず所属板の存在をチェックし、存在する場合は削除をブロックして管理者に警告ダイアログを出す安全装置を組み込みました。
// admin.js の削除ガード
async function deleteCategory(id, name, boardCount) {
if (boardCount > 0) {
alert(`カテゴリ「${name}」には ${boardCount} 個の板が所属しています。\n先に所属する板を別のカテゴリに移動するか削除してください。`);
return;
}
if (!confirm(`カテゴリ「${name}」を本当に削除しますか?`)) {
return;
}
// 削除APIリクエストの送信...
}
🎯 まとめと次回予告
第5回となる今回は、掲示板の秩序とナビゲーションの根幹である 「カテゴリ管理と階層ツリー」 の設計改善について解説しました。
今回の学び・ポイント
- 自由テキストの罠 :分類やメタデータは最初からマスターテーブル+ドロップダウンで固定すべし。
- カスケード同期の重要性 :カテゴリ名の変更が関連リソースへ即座に波及するトランザクション設計。
- 孤児データの防止 :リレーションを持つ親エンティティの安易な削除をUI・APIの両面でガードする。
しかし、カテゴリが整ったところで新たな要望が生まれます。
「ニュース速報を一番上に持ってきたい!」「深夜雑談カテゴリを上に上げたい!」
次回、 【第6回】トップページの使い勝手を極める!カテゴリ表示順のドラッグ&ドロップ並べ替え へと続きます!
🔗 関連リンク
- 本番サービス : https://2ch.biz/