← ブログ一覧に戻る
MIRAISアーキテクチャデータモデルPostgreSQL

MIRAIS 設計・実装記録 #2|どう組んだのか — 情報設計・アーキテクチャ・データモデル

第 2 回 どう組んだのか

前回で定めた要件を、実際の構造へ落とし込む回です。6 ロールを前提としたサイトマップ、未ログインへの情報遮断ポリシー、技術スタックの選定理由、そして 20 テーブルの星型スキーマまでを扱います。

第 IV 部 情報設計


12. サイトマップ

6 つのロール(未ログイン/学生/教員/事務員/参加企業/開発者)の権限とセキュリティ制約を満たす構成として設計しました。

├── 共通・一般公開エリア(未ログイン可)
│   ├── /                 トップページ
│   ├── /login            ログイン
│   └── /themes           研究テーマ一覧
│       └── /[id]         テーマ詳細(個人・企業情報・ポスターは完全非表示)
│
├── ログイン済ユーザー共通
│   └── /3d-venue         3D バーチャル展示会場
│
├── /student              学生専用
│   ├── /dashboard        テーマ登録・提出・マッチング進捗・投票
│   └── /archive          過年度アーカイブ
│
├── /teacher              教員専用
│   ├── /dashboard        ゼミ進捗モニター
│   └── /recommend        学科賞推薦入力
│
├── /company              参加企業専用
│   ├── /                 企業ダッシュボード
│   ├── /match            マッチング・投票管理
│   └── /venue            自社 3D ブース管理
│
├── /admin                運営事務専用
│   ├── /dashboard        全体ステータス・KPI
│   ├── /companies        参加企業管理
│   │   └── /[id]         企業詳細・担当者・参加履歴・受賞履歴
│   ├── /assets           提出資産管理・印刷物生成
│   ├── /awards           表彰選定
│   ├── /sync             外部データ同期
│   └── /settings         システム全体・開催期間・フェーズ設定
│
└── /backdoor             開発者専用(本番では無効)
    └── /debug            デバッグ・デモ用フェーズ切替

実装済みは 20 ページ(コンポーネント含め約 5,300 行)。設計時のサイトマップに対し、表彰選定画面と企業詳細画面の 2 ページが実装過程で追加されました。どちらもヒアリングで判明した運用の細部——序列に基づく企業賞の選出、毎年変わる担当者の管理——に対応するためのものです。


13. 主要ページの仕様

13-1. トップページ

既存サイトの機能を完全踏襲(F-01)。イベント概要、開催スケジュール、会場情報、お知らせ、研究一覧の抜粋、ログイン導線を配置しています。

認証状態で出し分けがあります。未ログインは公開用エンドポイントのみを参照し、ログイン済みは提出締切などイベントの全属性を取得します。さらに、未ログイン時は「提出」「締切」「リマインド」といった学生向けキーワードを含むお知らせを非表示にしています。学内向けの連絡が外部に見えてしまうことを防ぐためです。

13-2. ログインページ

3 つの認証手段を 1 画面に集約しています。

  1. Google ログイン(学内ユーザー)
  2. マジックリンク着地(参加企業。招待 URL を開くだけでログイン)
  3. 開発用ログイン(本番では非表示)

13-3. 研究テーマ一覧・詳細

一覧はキーワード検索とタグフィルタ、企業ログイン時は LLM 推薦枠を表示します。

詳細ページは、ログイン後に担当者名・所属ゼミ・ポスター PDF・コメント欄・投票ボタン・受賞バッジが解放されます。未ログインにはタイトルとアブストラクトのみで、それ以外は項目名(ラベル)も空白領域も残しません。

13-4. 学生ダッシュボード

学生が自ら入力する情報は「タイトル」「アブストラクト」「ポスターファイル」の 3 項目のみに制限しました。負担を最小限に抑えるためです。ゼミ・ブース番号・発表時間などは、すべて運営側の操作か自動割り当てで決まります。

提出は 2 ステップ UI(①タイトル・概要入力 → ②ポスター PDF のドラッグ&ドロップ)。加えて、教員・企業からのコメントの確認、企業オファーの受諾、他テーマへの投票ができます。

学籍番号の先頭 2 桁から入学年度を判定し、開催中のイベントと異なる年度の学生は自分の過去の提出物へ誘導する仕組みも入れています。卒業間近の学生がダッシュボードを開いて「何もない」となるのを防ぐためです。

13-5. 教員向け画面

担当ゼミの学生一覧と進捗(完了/タイトルのみ/未提出)を表示します。教員が参照できるのは自分が担当するゼミの学生だけで、全学生の一覧は取得できません。学科賞の推薦入力もここから行い、運営側へワンクリックで連携されます。

13-6. 企業向け画面

自社プロフィール(関心テキスト)の編集、LLM 推薦一覧、気になる研究のブックマーク、マッチング希望(オファー)の送信、順位付き投票、3D 会場内の自社ブース装飾(バナー・動画・説明文・座標)を提供します。

13-7. 運営向け画面

  • ダッシュボード:学生数・提出状況・企業数・投票数・コメント数の KPI と投票ランキング
  • アセット管理:全ポスターの確認、学生一覧、名札 A4 面付けプレビュー、ブース案内紙、ブース自動割り当ての実行
  • 表彰選定:ゼミ別推薦クォータの表示、推薦候補の自動充填、企業選出の実行、受賞の手動追加・変更・差し替え、企業序列の並び替え、表彰スライドの出力
  • 企業管理:承認、序列設定、招待リンク発行、担当者管理、過年度の参加・受賞履歴
  • 同期:スプレッドシートへのエクスポート、過年度 CSV のインポート、同期ジョブ履歴
  • 設定:イベントの作成・編集(年度・名称・開催日・会場・背景画像・テーマカラー・フェーズ・提出締切)

14. ロール別アクセス権限

ページ未ログイン学生教員参加企業事務員開発者
トップ・ログイン
研究一覧
研究詳細
3D 会場
学生エリア×××
教員エリア×××
企業エリア×××
運営エリア××××
開発者エリア×××××
  • 研究詳細の △:タイトルとアブストラクトのみ。それ以外はレスポンスに含まれません。
  • 3D 会場の △:ページは開けますが、未ログインは匿名ルームに隔離されるため他の参加者は見えず、ポスターの取得も認証が必要なため何も表示されません。

重要なのは、フロントエンドの表示制御はセキュリティ境界ではないという点です。 実際の境界は API 側の依存関係(認証必須・ロール限定・本人限定)にあります。画面を隠すことと、データを渡さないことは別の問題として扱いました。


15. 公開情報の遮断ポリシー

本システムで最も厳格に運用しているルールです。実装上も、公開用エンドポイントのモジュール冒頭にポリシーとして明記しています。

- 未ログインには研究の「タイトル」「抄録」のみを返す。
- 氏名・タグ・ゼミ名・ポスター URL・コメント・投票等は
  API から完全に除外する(DOM から丸ごと削除するという要件を担保するため)。
- 追加のフィールドを増やしてはならない。
  増やす場合は認証必須ルート側で提供する。

なぜここまで厳格にしたのか。 ポスター PDF には、担当学生の氏名・学籍番号・指導教員名・研究室名・共同研究企業名が含まれている可能性があります。研究発表という性質上、それらは紙面に書かれているのが普通です。したがって、ポスターファイルそのものを未認証で配信できるようにしてはならない。そして「タイトルと抄録だけ見せる」と決めた以上、その他のフィールドは存在しないものとして扱うのが最も安全だと判断しました。

公開用のレスポンス型を 3 フィールドだけのモデルとして定義したのは、将来この方針を知らない誰かがフィールドを足そうとしたとき、型の変更という明示的な行為が必要になるようにするためです。うっかりでは漏れない構造にしておく、という考え方です。



第 V 部 システムアーキテクチャ


16. 全体構成

                ブラウザ / スマートフォン
                          │  HTTPS
                          ▼
             ┌────────────────────────┐
             │  リバースプロキシ (nginx) │
             │   /api   → backend      │
             │   /docs  → backend      │
             │   /      → frontend     │
             └──────┬──────────────┬──┘
                    │              │
    ┌───────────────▼──┐      ┌────▼───────────────────┐
    │ frontend         │      │ backend                │
    │ Next.js 15       │      │ FastAPI                │
    │ React 19 / SWR   │      │ SQLAlchemy 2.0         │
    │                  │      │                        │
    │ 3D 会場を同梱     │      │ ポスター保管領域        │
    │ (Vite ビルド)     │      │ 認証情報               │
    └──────────────────┘      └────┬───────────────────┘
                                   │
                      ┌────────────▼────────────┐
                      │ PostgreSQL 16           │
                      └─────────────────────────┘

  外部連携(すべてオプション。未設定でも本体は動作する)
    ├─ 学内 LLM サーバー   … 埋め込みベクトル生成
    ├─ スライド生成 API    … 表彰/ブース案内スライド
    ├─ スプレッドシート API … 提出物/企業/表彰の書き出し
    ├─ ストレージ API      … 学生ポスターの保管
    ├─ Google Identity     … 学内 OAuth
    └─ チャットツール       … 未提出リマインド

構成上のポイントは、外部連携がすべてオプションであることです。学内 LLM が落ちていても推薦は動き、スライド API が未設定でもポータルは動き、チャット連携がなくてもリマインドは履歴として記録されます。外部サービスの障害が、システム全体の停止に波及しない構造にしました。

デプロイ先は学内の GPU サーバー(ARM アーキテクチャ)で、Docker Compose によるコンテナ構成です。開発の途中で学内サーバー上での運用許可が下りたため、現在はそこで稼働しており、継続的に発生するコストはありません。外部公開はリバースプロキシ経由のみで、フロントエンドとデータベースは直接公開していません。


17. 技術スタックと選定理由

17-1. バックエンド:Python + FastAPI

理由:入力検証が強く、業務ロジックを読みやすく保ちながら高速に API を構築できること。OpenAPI が自動生成されるためフロントとの契約が明示されること。そして、データサイエンス学部という文脈で Python はチーム全員が読める共通言語であること。

主要ライブラリは SQLAlchemy 2.0(Mapped / mapped_column の 2.0 スタイル)、Pydantic v2、python-jose(JWT)、httpx(外部 API 呼び出し)です。

17-2. フロントエンド:TypeScript + Next.js App Router

理由:UI と API の契約を型で固定でき、ルート単位で責務を分割しやすいこと。ファイルベースルーティングが、設計したサイトマップとそのまま 1 対 1 で対応すること。

特筆すべきは UI ライブラリを一切使っていないことです。Tailwind も Material UI も入れず、自前のデザイントークンとクラスだけで全ページを構成しました。「汎用テンプレ感の強い UI を避ける」という UI/UX 方針を守るには、既製のコンポーネント集から始めない方が早いと判断したためです。テーマカラーはイベント設定から動的に注入され、運営側が管理画面から変更できます。

17-3. データベース:PostgreSQL 16

理由:年度運用とロール別ワークフローに必要な整合性を保ちやすいこと。特に、受賞者の入れ替え操作で必要になる遅延評価される一意制約(トランザクション終了時まで制約の評価を遅らせる)は、他の軽量な選択肢では表現できません。この 1 点が選定の決め手になりました(第 21 章)。

17-4. 3D:React Three Fiber

企画段階では別の 3D ライブラリを想定していましたが、実装では React Three Fiber を採用しました。変更の理由は 3 点です。

  1. 見取り図から起こした 300 を超えるジオメトリを宣言的に組み立てる必要があり、React コンポーネントとして分割できる利点が大きかった。
  2. 物理演算とコリジョンによるアバター移動が必要だった。
  3. PDF テクスチャの動的な差し替えを、React の状態管理で扱いたかった。

物理エンジン、マルチプレイ同期、PDF レンダリングのライブラリを組み合わせています。

17-5. LLM:学内サーバー + フォールバック

学内 LLM サーバーが設定されていればそれを使い、未設定または応答しない場合は決定論的なモック埋め込みにフォールバックします。LLM が使えない環境でも推薦機能が停止しないことが設計の要点です(第 32 章 / 第 5 回)。

17-6. 認証:自前 JWT

外部の認証 SaaS を使わず、HS256 の JWT を自前で発行しています。理由は外部サービスに依存しないという前提と、企業向けマジックリンクという非標準のフローを自由に設計する必要があったためです。


18. リポジトリ構成

mirais/
├── docs/                  企画・議事録・設計
│
├── portal/                システム本体
│   ├── backend/           FastAPI(約 6,700 行)
│   │   └── app/
│   │       ├── main.py    エントリポイント・ルータ登録
│   │       ├── models.py  データモデル(20 テーブル)
│   │       ├── schemas.py API 契約スキーマ
│   │       ├── cli.py     管理 CLI
│   │       ├── core/      設定 / DB / セキュリティ
│   │       ├── api/       ルーティング層(17 モジュール)
│   │       └── services/  ビジネスロジック層(13 モジュール)
│   │
│   ├── frontend/          Next.js(約 5,300 行、20 ページ)
│   │   ├── app/           App Router
│   │   ├── components/    9 コンポーネント
│   │   └── lib/           API クライアント / セッション
│   │
│   ├── 3d-venue/          3D 会場(Vite + R3F、約 3,100 行)
│   ├── nginx/             リバースプロキシ設定
│   └── scripts/           起動・デプロイスクリプト
│
├── data/                  実データ(バージョン管理外)
│
└── Reference/             開発研究期間の技術検証(参照のみ・流用禁止)

Reference/ には検証期間の成果を保存しています。ポータルのデモ実装 2 世代、3D の検証 3 種、認証の検証、スライド生成の検証です。これらのコードは挙動と範囲の理解にのみ使い、本実装には流用しないというルールを設けました。検証コードは「動くこと」だけを目的に書かれており、そのまま製品に持ち込むと設計が濁るためです。



第 VI 部 データモデル


19. 設計思想:星型スキーマ

19-1. 中心にイベントを置く

要件は明快でした。毎年イベントを重ねてもデータが絶対に混ざらず、運営のノーコード設定と過年度アーカイブを同時に満たすこと。 そのために 2 つの原則を立てました。

  1. 年度 ID の徹底 — 学生の提出物、参加企業、投票、コメント、ゼミの所属に至るまで、その年限りのデータには必ずイベントへの外部キーを付与する。
  2. 不変データの分離 — ユーザー(学生や教員のアカウント情報)は年をまたいで在籍・勤務するため、「ユーザー基本情報」と「その年の提出物・ゼミ所属」を切り離して管理する。

19-2. この構造が課題をどう解決するか

過年度データの保護と検索 学生が卒業してアカウント情報が更新されても、過去の提出レコードには当時のタイトル・抄録・所属ゼミがその年度に紐づいて残ります。画面上で年度を切り替えるだけで、過去の資産を掘り起こせます。

二重管理とデザイン着せ替えのノーコード化 設定画面から新しい年度のイベントを作成すると、新しいイベント ID が発行され、空のダッシュボードが生成されます。背景画像も連携先 URL もこのイベント 1 レコードとして保存されるため、プログラムを 1 行も触る必要がありません

19-3. 7 テーブルから 20 テーブルへ

設計会議で確定した原案は 7 テーブル(イベント/ユーザー/ゼミ/提出物/企業/コメント/投票)でした。実装では 13 テーブルが追加され、20 テーブル構成になっています。追加分の大半は、ヒアリングで判明した運用の細部に対応するものです。

追加テーブル追加理由
ゼミ⇄教員の中間テーブル複数教員が担当するゼミを、単独ゼミと一貫して扱うため
企業担当者「毎年、担当者が変わる」「CC 管理が大変」への対応
表彰学科賞/企業賞の 2 系統と格上げワークフロー
お知らせ・FAQ・タイムテーブル現行サイトの機能踏襲(F-01)
招待トークン有効期限と使用履歴の管理
システム設定ノーコード設定の KV ストア
リマインド履歴送信結果の記録
ブックマーク・オファー企業の関心表明とマッチングのステータス管理
企業ブース3D 空間での装飾と座標
同期ジョブエクスポート・インポートの実行履歴

要件定義の段階で 20 テーブルを見通せていたわけではありません。ヒアリングで運用の細部を知るたびに、必要なテーブルが立ち上がってきた、というのが実際です。


20. テーブル構成の概要

20-1. 列挙型

ロール          : 事務 | 教員 | 学生 | 企業
フェーズ        : 準備中 | 提出受付中 | 投票期間中 | アーカイブ
提出ステータス  : 未提出 | タイトル済 | 完了
承認ステータス  : 保留 | 承認済
投稿者バッジ    : 先輩 | 同期 | 教員 | 企業 | 運営
オファー状態    : 保留 | 受諾 | 辞退 | 取下げ

すべて文字列+制約として定義しています。PostgreSQL のネイティブ列挙型は値の追加にスキーマ変更が必要でマイグレーションが重くなるため、意図的に避けました。

20-2. 中核テーブル

イベント 年度(一意)、イベント名、開催日、会場名・住所、連携先スプレッドシート URL、背景画像 URLテーマカラー、フェーズ、提出締切。年度に一意制約があるため、1 年度 1 イベントが構造的に保証されます。運営が触る設定はすべてこの 1 レコードに集約しました。

ユーザー メールアドレス(一意)、氏名、システムロール、学籍番号、チャットツール ID。年度への外部キーを持ちません。 ユーザーは年度をまたいで存在するためです。

ゼミ/ゼミ教員 ゼミは年度に紐付き、教員とは多対多で接続します。単独教員のゼミも複数教員のゼミも、同じ構造で一貫して扱えるようにしました。

学生提出物 年度、ユーザー、ゼミ、タイトル、抄録、タグ、ポスター URL、外部ストレージのファイル ID、提出ステータス、発表の部、ブース番号、埋め込みベクトル(年度, ユーザー) に一意制約があり、1 イベント 1 学生 1 提出が構造で保証されます。

企業 年度、企業名、連絡先、担当者名、CC 情報、関心テキスト、埋め込みベクトル、ロゴ URL、序列序列を手動調整したかのフラグ、招待トークン、承認ステータス。

序列に「手動調整したか」のフラグを持たせているのは、自動計算と手動並び替えを共存させるためです。運営が一度でも手で並び替えたら、以降その企業は自動計算の対象から外れます。人の判断を機械が上書きしない、という原則をデータ構造で表現しました。

コメント 年度、提出物、投稿者(学内ユーザー または 企業)、表示名のスナップショット、属性バッジ、本文。

表示名をスナップショットとして別途保持しているのは、投稿者が卒業・退職しても当時の表示を保つためです。過年度アーカイブが「当時のまま」であることを保証します。

投票 年度、提出物、投票者(学内ユーザー または 企業)、順位、スコア。 (年度, 企業, 順位) に一意制約があり、1 企業が同じイベント内で同じ順位を 2 回投票できません

表彰 年度、提出物、種別(学科賞/企業賞)、ラベル、授与元、企業。 (年度, 提出物) に一意制約——ただし遅延評価(第 21 章)。

20-3. 関係図

                       ┌──────────────┐
                       │   イベント    │ ← すべての年度データの中心
                       │  年度(一意) │
                       └──┬───────────┘
      ┌────────────┬───────┼────────────┬──────────────┐
      │            │       │            │              │
 ┌────▼─────┐ ┌────▼────┐  │      ┌─────▼──────┐ ┌─────▼────────┐
 │   ゼミ    │ │  企業    │  │      │  お知らせ   │ │ FAQ・式次第   │
 └────┬─────┘ └────┬────┘  │      └────────────┘ └──────────────┘
      │            │       │
 ┌────▼──────┐ ┌───▼──────────────┐
 │ ゼミ⇄教員 │ │ 企業担当者        │
 └────┬──────┘ │ 招待トークン      │
      │        │ 企業ブース        │
 ┌────▼───┐    └───┬──────────────┘
 │ユーザー │        │
 │(年度非依存)      │
 └────┬───┘        │
      │            │
 ┌────▼────────────▼──────────────────────┐
 │            学生提出物                    │
 │  一意制約(年度, ユーザー)                │
 │  埋め込みベクトル / ポスター / ブース番号 │
 └────┬───────────────────────────────────┘
      │
 ┌────┼──────────────┬──────────────┬──────────────┐
 │    │              │              │              │
┌▼────▼───┐   ┌──────▼─────┐  ┌─────▼──────┐ ┌─────▼──────────┐
│ コメント │   │   投票      │  │   表彰      │ │ブックマーク     │
│ バッジ   │   │  順位       │  │ 遅延制約    │ │オファー         │
└──────────┘   └────────────┘  └────────────┘ └────────────────┘

21. 設計上の勘所

21-1. 遅延評価される一意制約

表彰テーブルの (年度, 提出物) の一意制約は、トランザクション終了時まで評価が遅延されるよう定義しています。

理由は受賞者の差し替え(swap)です。2 件の表彰を入れ替える操作は 2 回の更新で構成され、その中間状態では一時的に同じ提出物が 2 行に存在します。即時評価される制約だと、この中間状態で制約違反となり操作が失敗します。評価をトランザクション終了時まで遅らせることで、入れ替えを 1 トランザクションで安全に実行できます。

この制約は PostgreSQL でなければ表現できません。 データベース選定の決め手のひとつになりました。「表彰者を入れ替えたい」という運用要件が、そのままデータベースの選定理由になる——要件と技術選定が直結した、気持ちのよい例だと思っています。

21-2. 排他的な外部キー

コメントと投票は「学内ユーザー」または「企業」のどちらかが主体になります。継承やポリモーフィック関連を使わず、null 許容の外部キーを 2 本並べるというシンプルな設計を採りました。ポリモーフィック関連は読みにくく、この規模では過剰だと判断したためです。可読性を優先しました。

21-3. 埋め込みベクトルの持ち方

埋め込みは JSON 配列としてテキスト列に格納しています。ベクトル検索用の拡張を使わなかった理由は 3 点です。

  1. 学内 LLM サーバーが使えるかが不確定だった。
  2. モック埋め込みでも動く構成にする必要があった。
  3. 現状の規模(100 件程度)では、全件走査とアプリケーション側でのコサイン計算で十分に速い。

規模が数千件を超えた時点でベクトル拡張へ移行する、という判断を先送りにしています。今の規模で必要ない最適化を先に入れない、という方針です。

スライド資料

MIRAIS 発表スライド / 「プロダクト紹介」がこの回に対応しますタップしてPDFを開く