MIRAIS 設計・実装記録 #3|サーバと画面 — バックエンドとフロントエンドの実装
第 3 回 サーバと画面
設計をコードにする回です。レイヤー分離、3 経路の認証設計、API のドメイン構成、そしてフロントエンドの構成と、認証必須のポスター PDF をブラウザに表示するための実装を扱います。
第 VII 部 バックエンド
22. アプリ構成
22-1. レイヤー分離
app/
├── main.py エントリポイント、CORS、起動処理、ルータ登録
├── models.py データモデル(永続化層)
├── schemas.py API 契約スキーマ
├── cli.py 管理 CLI
├── core/ 設定 / DB セッション / JWT
├── api/ ルーティング層(17 モジュール)— HTTP と認可のみ
└── services/ ビジネスロジック層(13 モジュール)— 純粋な処理
原則:業務ルールはルーターではなくサービス層に集約する。 実際、表彰アルゴリズムやスライド生成といった重い処理はすべてサービス層にあり、ルーティング層は入出力の変換と認可に徹しています。
この分離を守った効果は、テストのしやすさよりもむしろ読みやすさに現れました。「この機能はどこにあるか」を探すとき、HTTP の話なら api/、業務ロジックなら services/ と迷わず辿れます。
22-2. 起動時の処理
起動時にテーブルの自動作成を行い、開発モードの場合のみデモデータを投入します。本番モードではシードは一切走りません。本番でデータを投入する必要がある場合は、独立した管理 CLI を使う設計にしています。開発用フラグと本番のデータ投入を別の経路に分けることで、「開発フラグを立てないとデータが入れられない」という危険な依存を避けました。
23. 認証・認可の設計
23-1. 3 つの認証経路
① 学内ユーザー(学生・教員・事務)
ブラウザ → Google Identity → ID Token
→ バックエンドで ID Token を検証
→ メールアドレスからロールを解決、ドメインを検証
→ ユーザーを作成または更新 → 自前 JWT を発行
② 参加企業
運営が管理画面から招待リンクを発行(有効期限つきトークン)
→ 企業が URL を開くだけでログイン
→ トークンの期限と使用履歴を確認、未承認企業は拒否
→ 自前 JWT を発行
③ 開発用(本番では無効)
ロールを指定した即時ログイン。デモと検証の高速化のため
企業向けにパスワードを設定させない設計にしたのは、参加企業に新しいアカウントを作らせないという方針からです。年に一度しか使わないサービスのためにパスワードを管理させるのは、利用者にとって負担でしかありません。
23-2. 単一のトークン形式で 2 種類の主体を扱う
JWT のペイロードに「主体の種別」を持たせ、それが「ユーザーテーブルを引くか企業テーブルを引くか」を決めます。これにより、学内ユーザーと企業という異なるエンティティを 1 つのトークン形式で扱えるようになりました。
API 層は抽象化された「principal」オブジェクトだけを見ればよく、「ユーザーか企業か」の分岐が 1 箇所に閉じています。この抽象を最初に置いたおかげで、その後に増えた企業向け機能を、認証まわりを触らずに追加できました。
23-3. 認可の依存関係
認証必須 未認証なら拒否
学内ユーザー限定 企業トークンなら拒否
企業限定 ユーザートークンなら拒否
ロール限定 指定ロール以外は拒否
ロール解決には、運営・教員として登録されたメールアドレスを自動的に昇格させる仕組みがあります。既存ユーザーのロールは昇格方向にのみ自動更新され、降格は起きません。
23-4. 「現在のイベント」の解決
現在のイベント=年度が最大のもの、という単純な規則にしました。年度切替は「新しい年度のイベントレコードを作る」だけで完了します。日付による判定や手動フラグも検討しましたが、規則が単純であるほど間違いが起きにくいと考え、これを採りました。
24. API の構成
エンドポイントの網羅的な一覧は公開版では省略し、機能ドメイン単位の要約を記載します。
| ドメイン | 提供内容 | 認可 |
|---|---|---|
| 公開 | イベントの公開属性、研究のタイトルと抄録のみ | 認証不要 |
| 認証 | Google ログイン、招待リンクログイン、自身の情報取得 | 一部認証必須 |
| イベント | イベント CRUD、タイムテーブル、FAQ、お知らせ、ゼミ、当日の「現在の部」 | 参照は公開/更新は運営 |
| 提出物 | 一覧・詳細、自分の提出物の取得と更新、受賞情報 | 認証必須 |
| 企業 | 運営向け CRUD と招待リンク発行、企業自身の関心・推薦・ブックマーク | 運営/企業 |
| コメント | 属性バッジ付きの一覧と投稿 | 認証必須 |
| 投票 | 順位投票・評価投票、集計 | 認証必須 |
| マッチング | 意味検索、埋め込みの再構築 | 一部運営限定 |
| オファー | マッチング希望の送信とステータス変更 | 企業/学生 |
| ブース | 企業ブースの取得・更新、3D 描画用の一覧 | 企業/認証必須 |
| ポスター | PDF の配信とアップロード | 認証必須/学生限定 |
| 運営 | ダッシュボード、学生一覧、リマインド、設定、表彰選定、企業詳細、ブース割当、スライド出力 | 運営限定 |
| 同期 | スプレッドシート書き出し、過年度 CSV 取り込み、ジョブ履歴 | 運営限定 |
| 教員 | 自ゼミの進捗、学科賞推薦、投票ランキング | 教員限定 |
| 学生 | 所属年度の判定と誘導先の指示 | 学生限定 |
| 開発 | シード、フェーズ切替、接続状況 | 本番では無効 |
24-1. 提出ステータスの自動判定
学生に「提出しました」ボタンを押させる代わりに、入力内容から自動でステータスを決めています。
タイトル + 抄録 + ポスター が揃っている → 完了
タイトル のみ → タイトル済
どちらもない → 未提出
「2 週間前までにタイトルと概要、その後にポスター」という実際の運用が 2 段階であることに対応しています。学生からすると、入力するだけで状態が進むので、提出したつもりが未提出だった、という事故が起きません。
24-2. ポスターアップロードの検証
学生からのファイル受け取りは、システムの中で最も慎重に扱うべき箇所です。以下を多段で確認しています。
- ロールが学生であること
- 学籍番号が登録済みで、数字のみであること
- 現在のフェーズが受付中であること
- Content-Type と拡張子が PDF であること
- ファイル先頭のバイト列が PDF の署名であること
- ファイルサイズが上限以内であること
保存先のファイル名はログイン中のユーザーの学籍番号から生成され、リクエストパラメータには由来しません。他人のファイルを上書きする経路を、そもそも作らないという設計です。
第 VIII 部 フロントエンド
25. 構成と API 契約
25-1. App Router とサイトマップの一致
設計したサイトマップが、そのままディレクトリ構造に対応します。この 1 対 1 対応は設計段階から意図していました。設計書とコードの構造が一致していると、「この画面を直したい」から「このファイルを開く」までの距離がゼロになります。
すべてのページをクライアントコンポーネントとし、データ取得は SWR で行っています。サーバーサイドレンダリングを使っていない理由は、①認証トークンをブラウザ側で保持しているためサーバー側でセッションを持てない、②単一コンテナ構成でシンプルさを優先した、の 2 点です。
25-2. API クライアント
型定義と HTTP クライアントを 1 ファイルに集約しました。バックエンドのスキーマと対になる TypeScript 型を定義しており、レスポンス型は 30 種類以上あります。
クライアントの挙動は以下の通りです。
- 保存されたトークンを Authorization ヘッダに自動付与
- JSON ボディには Content-Type を自動設定(ファイル送信時は除く)
- 常に最新を取得(キャッシュしない)
- 非 2xx はカスタムエラーとして throw し、サーバーのエラーメッセージを採用
エラーメッセージをサーバー側の文言そのまま表示する設計にしたのは、バックエンドで日本語のわかりやすいメッセージ(「PDF ファイルのみアップロードできます」など)を返しているためです。フロント側で英語のステータスコードを翻訳し直すより、発生源で書いたほうが正確になります。
25-3. 認証状態の管理
セッションは SWR のキーとして自身の情報取得エンドポイントを参照し、フォーカス時の再検証を無効にしています。ログアウトはトークンの破棄とキャッシュの無効化のみです。サーバー側にセッションを持たないステートレス設計のため、これで完結します。
26. コンポーネント設計
| コンポーネント | 役割 |
|---|---|
| ナビゲーションバー | ロール別にリンクを動的切替。ログイン状態で識別チップとログアウトを表示 |
| ロールガード | 許可ロール以外に「アクセス権限がありません」を表示 |
| フェーズバッジ | 運用フェーズを日本語ラベルで表示 |
| 統計グリッド | KPI カードのグリッド |
| 研究カード | ゼミ・ブース番号・マッチ率をピルで表示 |
| ドロップゾーン | ドラッグ&ドロップによるファイル受け取り |
| ポスタービューア | 認証付きで PDF を取得し、blob 経由で表示 |
| コメントスレッド | 属性バッジ付きのスレッド表示 |
| 投票パネル | 企業は順位投票、それ以外は評価投票 |
26-1. ポスタービューアの設計判断
ポスター PDF は認証ヘッダが必須です。しかし <img> や <iframe> の src 属性はカスタムヘッダを送れません。そこで以下の手順を取りました。
① 認証ヘッダを付けて PDF を取得
② 取得したデータから blob URL を生成
③ その blob URL を iframe に流し込む
認証必須の資産をブラウザで表示するための定石であり、第 15 章 / 第 2 回の公開遮断ポリシーを崩さずに認証ユーザーへ表示するための実装上の要になっています。「ポスターを認証必須にする」という判断は、こういう実装コストとセットで引き受けるものだ、というのが学びでした。
26-2. デザインシステム
グローバル CSS に自前のクラスを定義しています。カード、ボタン、バッジ、ピル、フェーズ表示、入力欄、ヘッダ——いずれも用途から名前を付けました。テーマカラーはイベント設定から動的に注入されるため、運営が管理画面から変更した色が全ページに波及します。