学部の年次発表会を、準備から表彰まで1つのポータルに統合した ― MIRAIS 設計ノート
学部の年次発表会を、準備から表彰まで1つのポータルに統合した
MIRAIS / 未来創造発表会 統合ポータルシステム — 設計ノート(要約版)
武蔵野大学データサイエンス学部の年次成果発表会「未来創造発表会」を、準備から表彰までひとつの導線に統合したポータルシステムを、3 名のチームで設計・実装しました。私はプロジェクト代表として、全体設計とバックエンドを担当しています。
この記事は、その設計・実装記録の要約版です。全文は 6 回のシリーズとして別途公開しており、学内発表で使用したスライドも記事の末尾に添付しています。
技術の一覧ではなく、制約の中でどう判断したかを中心に書きました。実装の細部より、そちらのほうが読んでくださる方にとって価値があると考えたためです。
3 行でいうと
- 学部の年次発表会の運営は、フォーム → スプレッドシート → サイトへの手動転記と、チャットでの個別連絡で回っていた。
- それを、認証・提出・マッチング・3D 展示・投票・表彰・印刷物生成まで 1 つのポータルに統合した。
- 外部の商用サービスに依存しないという前提を最初に置いたことが、結果として外部依存に強いアーキテクチャを生んだ。
| 項目 | 値 |
|---|---|
| バックエンド | 約 6,700 行 / 17 ルータ / 13 サービス(Python + FastAPI) |
| フロントエンド | 約 5,300 行 / 20 ページ(TypeScript + Next.js App Router) |
| 3D 会場 | 約 3,100 行 / 12 ファイル(React Three Fiber) |
| データモデル | 20 テーブル(PostgreSQL 16) |
| ロール | 4 + 未ログイン + 開発用 |
| 稼働環境 | 学内 GPU サーバー(継続コストなし) |
出発点:手作業が毎年繰り返されていた
発表会の運営は、こんなフローで回っていました。
1. 企業へ案内を出し、参加企業を集計する
2. 学生向けフォームを作り、研究テーマを回答させる
3. 集めたテーマをスプレッドシートにまとめ、サイトへ手で転記する
4. 誰が 3 部制のどのタイミングで発表するかを割り振る
5. ポスター印刷や登校時間の案内を、個別に連絡する
学生側も「フォームに入力し、ポスターを作り、分かりにくい場所へ保存し、各自で印刷して当日まで保管する」という流れです。そして参加企業がどう体験しているかは、運営側から見えていませんでした。
目的は、ひとつの文にまとめました。
未来創造の参加者全員が、準備から表彰までをこのポータルで完結させ、これまで感じていた不便をこの代から解消する。
同時に、これを「ハッカソンの提出作品」で終わらせず、来年度以降も動き続けるインフラとして設計することを前提条件に置いています。作った人間が卒業した後も回り続けなければ、解決したことにならないからです。
外部に依存しない、という前提が設計を決めた
企画の段階で、外部の商用サービスに依存せず、学内の計算機資源だけで完結させるという前提を置きました。この前提が、以下の判断を直接規定しています。
- 商用 LLM API に依存せず、学内の GPU サーバー上で動く LLM を第一候補にする。かつ、それが使えない環境でも機能が停止しない構成にする。
- 認証基盤に外部 SaaS を使わず、Google ID Token の検証と自前の JWT 発行で賄う。
- ホスティングは学内の計算機資源を利用する。
開発の途中で学内サーバー上での運用許可が下りたため、現在は学内の GPU サーバーで稼働しています。継続的に発生するコストはありません。
結果として、こういう構造になりました。
外部連携(すべてオプション。未設定でも本体は動作する)
├─ 学内 LLM サーバー … 埋め込みベクトル生成
├─ スライド生成 API … 表彰/ブース案内スライド
├─ スプレッドシート API … 提出物/企業/表彰の書き出し
├─ ストレージ API … 学生ポスターの保管
├─ Google Identity … 学内 OAuth
└─ チャットツール … 未提出リマインド
学内 LLM が落ちていても推薦は動き、スライド API が未設定でもポータルは動き、チャット連携がなくてもリマインドは履歴として記録されます。
この前提がなければ、素直に商用 API を呼んで、それが落ちた日にすべてが止まっていたはずです。「使えるものが限られているから諦める」ではなく「限られているからこの構成になる」という順序で考えられたことが、結果的にアーキテクチャを筋の通ったものにしました。
ヒアリングの一言が、データモデルを決めた
運営を担当されている事務職員の方に、年間スケジュールに沿って業務を伺いました。このプロジェクトで最も情報密度が高い一次情報です。
そこで出た 「現在管理しているスプレッドシートと同じ形にならなくてもよい」 という一言が、システム側のデータベースを正暦(Single Source of Truth)とする設計判断を可能にしました。この一言がなければ、既存のスプレッドシート形式に引きずられた、はるかに窮屈なデータモデルになっていたはずです。
さらに、雑談のように出てきた要望が、そのまま機能になっています。
| 伺った言葉 | 生まれた機能 |
|---|---|
| 「毎年、背景画像を変更できるようにしたい」 | ノーコード運営設定画面(デザイン要素をデータとして持つ) |
| 「切る前の紙を作るところまでは自動化できそう」 | 名札の A4 面付けデータ生成 |
| 「ポスターの上に貼る、誰がいつ発表するかの案内紙」 | ブース案内スライドの自動生成(1 枚 = 1 ブース位置) |
| 「テンプレートを用意して、それをコピーしてデータを入力する方式が良い」 | 表彰スライド自動生成(テンプレ複製+プレースホルダ置換) |
| 「2 週間前までに集める。遅れる前提でスケジュールを組んでいる」 | 自動リマインダーを「あれば良い機能」から必須機能へ格上げ |
何を自動化しないかを決める
いちばん学んだのは、逆の判断のほうでした。
企業ロゴの収集は、先方の稟議が絡んで時間がかかり、運営の方が手作業で配置しています。これを自動化しようとすれば、先方の稟議プロセスまで巻き込む大掛かりな話になります。しかし「運営の方が手でコピペする」という運用を前提に置けば、必要なのは画像 URL を保存する 1 カラムと、それを貼り付ける入力欄だけです。
スプレッドシートとの同期も、双方向ではなく一方向の書き出しに限定しました。双方向同期は競合解決が複雑になり、データ破損のリスクを生みます。一方向に限ったことで、同期ロジックは壊れず、スプレッドシート側を誰かが手で編集しても DB は汚染されず、スプレッドシートは「バックアップ兼、運営が見慣れたビュー」として機能するようになりました。
システムで運用を強制するのではなく、既存の運用を許容した上で、それが最小手数で済む機能を用意する。 「ユーザーファースト」を、要望を全部叶えることだと誤解しないこと。相手の運用のどこに手を入れないかを決めることも、同じくらい設計の一部だと考えています。
画面を隠すことと、データを渡さないことは別問題
このシステムで最も厳格に運用しているルールです。
ポスター PDF には、担当学生の氏名・学籍番号・指導教員名・研究室名・共同研究企業名が含まれている可能性があります。研究発表という性質上、それらは紙面に書かれているのが普通です。
そこで、未ログインに対しては次のポリシーを立てました。
- 未ログインには研究の「タイトル」「抄録」のみを返す。
- 氏名・タグ・ゼミ名・ポスター URL・コメント・投票等は
API から完全に除外する(DOM から丸ごと削除するという要件を担保するため)。
- 追加のフィールドを増やしてはならない。
増やす場合は認証必須ルート側で提供する。
「フロント側で出し分ければよい」という案もありましたが、それでは通信内容を見れば取得できてしまいます。CSS で隠すのはもちろん論外です。API レスポンスに載せないことが唯一の確実な担保だと判断し、公開用のレスポンスモデルを 3 フィールドだけを持つ型として定義しました。
型にした理由は、将来この方針を知らない誰かがフィールドを足そうとしたとき、型の変更という明示的な行為が必要になるようにするためです。うっかりでは漏れない構造にしておく、という考え方です。
その代償も引き受けています。ポスター PDF が認証必須になったため、<img> や <iframe> の src では表示できません(カスタムヘッダを送れないため)。認証ヘッダ付きで取得 → blob URL を生成 → iframe に流し込む、という手順を踏んでいます。判断は、実装コストとセットで引き受けるものだという学びでした。
運用をそのままアルゴリズムに翻訳する
ヒアリングで分かった表彰の運用は、こういうものでした。
- 参加企業には序列があり、手動で管理されている。
- ゼミ側から学科賞の候補リストを受け取り、そこから企業賞へ格上げする。
これを 2 フェーズのアルゴリズムに翻訳しました。
Phase 1(教員推薦):合計 30 名の推薦枠を、ゼミの学生数比率で配分します。単純な四捨五入では合計が 30 になりません。切り捨てでは枠が余ります。そこで Hamilton 法(最大剰余方式)——比例配分の小数部が大きい順に余りを 1 ずつ配る——を使い、合計を厳密に保ちながら比例性を最大限維持しました。タイブレークを 3 段階にしているのは、実行のたびに結果が変わらない決定性のためです。「昨日と今日で推薦枠が違う」というのは、運営の信頼を失う類のバグです。
Phase 2(企業選出):承認済み企業を序列の高い順に巡回し、各社の順位投票を辿って「推薦プールにあり、まだ他社に確保されていない」提出物を確保、そのレコードを「学科賞 → 企業賞」に格上げします。
この設計が体現しているルールは 4 つです。
- 企業賞は必ず学科賞の推薦プールから選ばれる。教員が推薦していない学生が、企業の一存で受賞することはない。
- 序列の高い企業から先に選ぶ。
- 1 学生 1 賞。
- 冪等。何度実行しても同じ結果になる。
最後の冪等性は、最初の実装では満たせていませんでした。前回選出済みの企業賞を推薦プールへ戻す処理を入れておらず、「実行するたびに推薦候補が減っていく」というバグとして発現しています。冪等性は後から足すものではなく、最初から設計に織り込むものだと痛感しました。
そしてもうひとつ。統合実行では、推薦プールが空のときだけ Phase 1 を実行し、既に推薦があるときは上書きしません。教員が調整したプールを、運営が自動実行ボタンを押した拍子に消してしまわないためです。自動化は、人の判断を上書きしない範囲で行う。 ロゴのコピペ運用を許容した判断と、同じ発想です。
LLM が落ちても、推薦は止まらない
企業の関心テキストと研究の抄録をベクトル化し、コサイン類似度で推薦する機能です。学内の LLM サーバーを使いますが、それが使えない前提も置きました。
学内 LLM サーバーが設定されていない → モック埋め込みを使う
設定されている → API を呼び、成功すれば正規化して使う
呼び出しが失敗した → モック埋め込みにフォールバック
フォールバック側も、真面目に作っています。日本語トークンを 2-gram に展開し("機械学習" → ["機械学習", "機械", "械学", "学習"])、256 次元のハッシュトリックでバケットに加算して L2 正規化する、という構成です。形態素解析器を依存に加えずに日本語の部分一致を捉えられ、語彙表を持たないので未知語にも対応でき、メモリも固定です。
そして決定論的であること。同じ入力に対して常に同じベクトルを返すため、実演の再現性が保証されます。
これは要件の実装であると同時に、実演の場での保険でもありました。数分のデモ中に外部依存が落ちる可能性を、設計で消しておく。「動かないかもしれない」という不安を抱えたまま人前に立たなくて済むのは、想像以上に大きな効果がありました。
3D 会場を「作ってみた」で終わらせない
今年度、通信制の学部が新設されました。遠隔地の学生の展示の場をどう確保するか——3D バーチャル会場は、この課題への直接的な解として位置づけています。
「3D 技術を使ってみたい」という動機が先にあったのは事実です。それを技術の披露で終わらせず、目の前にある具体的な課題への解答として設計し直したことが、このプロジェクトで私が最もこだわった点のひとつでした。
実際の会場の見取り図から座標を抽出し、3D 空間へマッピングしています。ここで意図的な「嘘」をひとつ入れました。建物の平面を実寸の 2 倍にスケールし、高さは等倍のままにしています。実寸だとアバターで歩き回るには窮屈だったためです。ただしブースの相対的な位置関係は正確なので、「どのエリアにどの研究があるか」という本来の目的は損なわれません。何を正確に保ち、何を犠牲にするかを決めるのが設計だと考えています。
そして最後のピースが、現地との同期でした。
運営が「今は 2 部です」と設定すると、3 秒以内に会場内の全 36 面のポスターが 2 部の発表者のものへ自動で切り替わる。
現地の入れ替えと 3D 空間の入れ替えが同期する。ここまでやって初めて、3D 会場は展示の代替になります。
なお、実データを扱う部分がいちばん手間でした。CSV から取り込んだブース番号は "1-27" "27" "01-05" " 27 " と表記がばらつき、発表の部も "1" "part_1" 1 が混在しています。現実のデータは綺麗ではないという前提に立ち、読み取り側で吸収する設計にしました(過去データを書き換えると、当時の記録が失われるためです)。
率直に、残っていること
外部公開版なので具体的な箇所は伏せますが、性質だけ書いておきます。最優先の 3 つはこれです。
- 認可の粒度の不一致。公開遮断ポリシーを厳格に定めた一方で、後から追加した一部の機能でその適用が徹底されていませんでした。ポリシーを文章とコメントで守るだけでは不十分で、型や共通の依存関係で機械的に強制する仕組みが必要だったと考えています。
- スキーマ変更の仕組みが未導入。開発初期に「起動時に自動でテーブルを作る」方式を選んだことに起因します。開発中は快適でしたが、運用開始後にカラムを追加する手段がありません。運用フェーズに入る前に必ず解消すべき負債です。
- 自動テストの不在。率直に言って最大の課題です。表彰アルゴリズムのように分岐が多く、間違えたときの影響が大きい処理が、人手の確認だけで担保されている状態です。機能を増やすより先に、ここを埋めるべきだと考えています。
ほかに、流量制限・監査ログ・依存脆弱性の継続検査・構造化ログと監視が未整備です。リマインダーの自動実行も、唯一「部分実装」のまま残っています(学内チャットツールへの Bot 導入に申請手続きが必要で、技術ではなく組織的な段取りが律速になっている箇所です)。
学んだこと 3 つ
1. 制約は設計を明確にする
「外部の商用サービスに依存しない」という前提は、最初は不自由に見えました。しかし実際には「商用 API に依存できない」→「学内資源を使う」→「それが使えないときも動く構成にする」という判断の連鎖を強制し、結果として外部依存に強いアーキテクチャを生みました。開発途中で学内サーバーでの運用許可が下りたことで、この構成はそのまま「継続コストのかからない運用」にも繋がっています。
2. 相手の運用のどこに手を入れないかを決める
ロゴの収集を自動化しないと決めたこと、同期を一方向に限定したこと、「テンプレをコピーして入れる」という既存の作業モデルに合わせたこと。いずれも「できるのにやらなかった」判断です。システムが解決すべきなのは相手の課題であって、相手の業務を作り変えることではない。何を作らないかを決めるのも設計のうちです。
3. 要件定義は最初に完成しない。だから軸を先に決める
企画の初期に、チームでこう合意しました。
細かい要件定義は今後たくさん変わる。ですが、コアな体験や本質的な部分は変えずに貫いていきたい。
実際、要件は大きく変わりました。テーブルは 7 個から 20 個に増え、設計になかった機能が 11 個増えています。それでも迷子にならなかったのは、変わってよいものと変えてはいけないものを最初に線引きしていたからです。変化に強い計画とは、詳細な計画ではなく、軸が明確な計画のことだと思っています。
全文(設計・実装記録シリーズ)
要件定義からデータモデル、アルゴリズムの詳細、残課題まで、内部資料と同じ密度で書いた全文を 6 回に分けて公開しています。
- なぜ作ったのか — 課題認識・ヒアリング・要件定義
- どう組んだのか — 情報設計・アーキテクチャ・データモデル
- サーバと画面 — バックエンドとフロントエンドの実装
- 3D バーチャル会場 — 見取り図から歩ける空間へ
- 頭脳部分 — マッチング・表彰・割り当てのアルゴリズムと外部連携
- 守り方と到達点 — セキュリティ設計・残課題・学んだこと
このプロジェクトを始めたきっかけは、率直に言えば「発表会のサイトを作りたい」という程度のものでした。それが最終的に 20 テーブルのデータモデルと 3 つのアプリケーションからなるシステムになったのは、関係者の方にお時間をいただいて話を伺えたからです。
想像で作っていたら、名札の面付けも、ブース案内紙も、表彰スライドのテンプレート方式も、企業の序列も、どれ一つ実装されなかったはずです。それらは全部、実際に困っている人の話の中にしかありませんでした。
コードは読めば分かります。判断の理由は、書き残さなければ消えます。