メインコンテンツまでスキップ

備品管理システム仕様書

1. システム概要​

1.1 目的​

本システムは、会津大学学園祭(通称: 蒼翔祭、略称: 学祭)実行委員会(学祭委員) における備品管理の中核を担うシステムである。

学祭で使用する備品の貸出・返却・在庫管理を効率化し、リアルタイムで備品の状態を把握できる環境を提供する。

1.2 システム構成の基本方針​

Panasonic FZ-N1 (Android 6.0.1) を主たる操作端末(メインインターフェース)とし、現場での高速かつ確実な備品操作を実現する。

Web管理画面(Sho-Roomモジュール) は、マスタ管理、備品割当、高度な検索、ラベル発行・貼付確認、履歴確認等の管理的役割を担い、FZ-N1アプリを補完する。「学園祭実行委員」Discordロールへ付与されるEQUIPMENT_VIEWで閲覧機能を利用でき、編集・削除・マスタ管理等は操作ごとに必要な追加の権限ビットで制御する。

前日の団体貸出では、教室に常駐する学祭委員が団体QRコードをFZ-N1で読み取り、ログイン済みの端末を参加団体の担当者へ手渡す。担当者は同じFZ-N1で対応する備品を一つずつスキャンし、端末を学祭委員へ返却する。各団体が備品をテント等の設置場所へ搬出完了した後、学祭委員(備品PJ)がFZ-N1を持って各設置場所を巡回し、学祭委員またはその場に居合わせた参加団体の担当者が対象備品を一つずつ二重確認スキャンして配置完了を確認・確定する。参加団体向けの専用Web画面や追加の認証入力は使用しない。

1.3 対象ユーザー​

  • 学祭委員: Discordロールごとに付与された権限ビットの範囲で、FZ-N1またはSho-Roomを利用する。「学園祭実行委員」DiscordロールにはEQUIPMENT_VIEWを付与し、備品の閲覧、通常の団体貸出セッション、貸出・配置確認スキャンを利用可能とする

  • 備品管理権限を持つ学祭委員: EQUIPMENT_EDIT、EQUIPMENT_DELETE、EQUIPMENT_MANAGE_LOCATION、EQUIPMENT_MANAGE_ORGANIZATION、EQUIPMENT_MANAGE_TYPE、EQUIPMENT_BULK_OPERATION等、担当業務に必要な権限ビットが付与された範囲でマスタ、割当、ラベル、例外処理、備品状態の強制変更を管理する

  • システム管理権限を持つ学祭委員: SYSTEM_MANAGEが付与されている場合に、権限確認、監査・通知情報、貸出セッションの強制終了を管理する

  • 年度更新権限を持つ学祭委員: EQUIPMENT_RESETが付与されている場合に、年度更新を実行する

  • 参加団体の担当者: 学祭委員から手渡されたFZ-N1で、団体に割り当てられた備品を貸出時にスキャンする。システムへ直接ログインしない

1.4 スコープ​

対象範囲(Scope In):

  • FZ-N1端末アプリ(Java / Android 6.0.1)によるNFCログイン、個別スキャン、通常の団体貸出セッション、一括操作(例外処理)、状態変更、オフライン操作

  • 前日の団体貸出における学祭委員立会い、団体QR読み取り、FZ-N1上の貸出セッション、貸出時の備品スキャン、搬出後の二重確認スキャン

  • スパイスとの回収タイミング交渉結果に応じた返却運用(D-02〜D-05)。最終決定前のパターンA/Bを保持する

  • 申請システムの企画別必要数を利用した備品割当

  • 講義棟出展用備品を除外した割当と、同一教室を優先する割当

  • Sho-Room(React)による備品、場所、団体、企画、備品種別のマスタ管理

  • Sho-Room Workerの既存BFFによる備品管理APIへの中継。共通認証基盤が設定したCookieからJWTを抽出し、Authorizationヘッダーへ変換して中継する

  • Sho-Roomによる備品割当、在庫状況、貸出状況、要対応備品、操作履歴の確認

  • 備品の全項目に対する変更履歴と、スキャン履歴の記録

  • Discordロールから自動計算される権限ビットによる権限管理

  • NFCログイン成功時のDiscordプライベートスレッド作成、本人メンション、送信15分後の削除

  • ラベル印刷連携(Epson TM-L90 / ラベル印刷エージェント経由)と貼付後確認

  • 学祭準備期間における大学環境cronサーバー向け全データエクスポートAPI

  • 学祭倉庫に保管されるコーン、バケツ、消火器等の備品管理

  • 特殊操作(備品状態の強制変更、権限確認)

  • 年度更新(全備品リセット、進行中の貸出セッションの終了、履歴のアーカイブ)

対象外(Scope Out / Out of Scope):

  • Google SpreadsheetとのExport / Import

  • 共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)の認証・セッション基盤の新規設計・構築。OAuthフローとCookieの設定・削除はlogin-api、JWT発行はdiscord-auth-worker等、JWT検証・BAN管理はunified-auth-workerが担当するため、本システムは既存基盤を利用する

  • 共通認証基盤が担うCookie/JWTの発行・保管・有効期限・失効・ローテーション、CSRF・XSS対策などの認証・セッション詳細。ただし、Sho-Room Workerの既存BFFによるCookieからJWTへの変換と備品管理APIへの中継は対象範囲に含む

  • 学祭委員名簿(Staffs)の登録・管理。外部システムからsoshosai-system-dbへ供給される前提とする

  • Discord DMによるログイン通知

  • 過年度データの別基盤への移行

  • 団体識別用QRコードの発行・生成・配布。出展者サーバーの各団体チャンネルへの配布は別botの担当であり、本システムの対象外とする

1.5 ユースケース一覧​

IDユースケース名概要優先度
UC-01NFCログインFZ-N1で学生証をタッチし、学籍番号で名簿照合してログインする高
UC-02Discordログイン通知ログインした本人へ一時的なプライベートスレッドで通知する高
UC-03個別スキャン備品QRを読み取り、状態を照会・更新する高
UC-04団体一括操作(例外処理)団体QRから対象備品を確認し、権限を持つ学祭委員が例外的に一括で状態を更新する。通常の団体貸出には使用しない高
UC-05オフライン操作・同期通信不通時の操作を端末へ保存し、復旧後に同期する中
UC-06備品マスタ管理備品の登録、編集、論理削除、検索を行う高
UC-07各種マスタ管理場所、団体、企画、備品種別を管理する中
UC-08ダッシュボード確認在庫、貸出、要対応備品、最近の操作を確認する中
UC-09ラベル発行・貼付確認備品ラベルを印刷し、発行・貼付確認の記録を残す高
UC-10備品割当申請数と除外条件に基づき、企画へ備品を割り当てる高
UC-11団体貸出セッション(教室での学祭委員立会い)学祭委員が団体QRで貸出セッションを開始し、担当者がFZ-N1で備品を一つずつ確認して持ち出した後、テント等で二重確認スキャンを行い配置を確定する高
UC-12年度更新学祭終了後、備品状態を初期化し履歴をアーカイブする中
UC-13特殊操作理由付き強制変更、権限確認を行う低
UC-14外部データ連携大学環境のcronサーバーが全備品データを取得する低
UC-15操作履歴確認備品の変更履歴、スキャン履歴、実行者・端末情報を確認する高
UC-16共通認証・セッション基盤の利用共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)が提供する既存のOAuth、Cookie、JWT発行・検証を利用する。認証・セッション基盤の詳細設計は本システムの対象外対象外
UC-17既存BFF経由の備品管理API利用Sho-Room Workerが既存BFFとしてブラウザのCookieからJWTを取り出し、Authorizationヘッダーへ変換して備品管理APIへ中継する。既存BFFへの備品管理API接続と中継は本システムの対象範囲とする高

1.6 機能要件一覧​

優先度はMoSCoW(Must / Should / Could / Won't)で管理する。共通認証・セッション基盤の新規設計・構築は「Scope Out」とする一方、Sho-Room Workerの既存BFFによる備品管理APIへの中継はScope Inとして機能要件に含める。

ID機能名説明優先度関連UC
F-001NFCログイン学生証から学籍番号を読み取り、名簿照合後にJWTを発行するMustUC-01
F-002Discordログイン通知ログイン成功後にプライベートスレッドを作成し、本人を追加・メンションして利用情報を通知し、メッセージ送信から15分経過後に通知スレッドを削除するMustUC-02
F-003個別スキャン・照会備品QRから現在の情報と状態を取得するMustUC-03
F-004個別状態変更貸出状態および備品状態を事前定義された選択肢から変更するMustUC-03
F-005団体一括操作プレビュー(例外処理)団体QRから対象備品と種別別数量を表示する。通常の団体貸出では使用せず、権限を持つ学祭委員の例外処理に限定するMustUC-04
F-006団体一括操作(例外処理)通常の団体貸出はUC-11の貸出セッション方式に一本化し、本機能は指定外備品の救済や運用上の例外に限って、EQUIPMENT_BULK_OPERATIONを持つ学祭委員が対象備品を確認して一括更新する。返却はスパイスとの回収タイミング交渉結果に応じたD-02〜D-05の2パターンで扱い、最終決定は保留するMustUC-04
F-007オフライン記録通信不通時の操作を端末内へ保存するShouldUC-05
F-008オフライン同期通信復旧後に保存データを送信し、競合を処理するShouldUC-05
F-009備品検索・一覧条件検索、ソート、ページネーションにより備品を表示するMustUC-06
F-010備品登録・編集・削除Sho-Roomから備品を登録、編集、論理削除するMustUC-06
F-011備品QRスキャン(Web)ブラウザカメラでQRを読み取り、備品詳細を開くShouldUC-06
F-012場所マスタ管理場所を登録、編集、削除するShouldUC-07
F-013団体マスタ管理団体を登録、編集、削除するShouldUC-07
F-014備品種別マスタ管理備品種別を登録、編集、削除するShouldUC-07
F-015ダッシュボード総数、貸出中、在庫、要対応件数、最近の操作を表示するShouldUC-08
F-016場所別統計元の場所と現在地・持出先の観点で集計するShouldUC-08
F-017ラベル印刷QR、元の場所、持出先、使用団体、使用企画をラベルへ印刷するMustUC-09
F-018ラベル発行管理発行番号、発行日時、発行者、再発行理由を記録するMustUC-09
F-019貼付後確認教室・場所単位で貼付済みラベルをスキャンし、重複と不足を検知するShouldUC-09
F-020申請数取込申請システムから企画別・備品種別別の必要数を取得するMustUC-10
F-021備品割当案作成除外条件と同一教室優先条件に基づき割当案を作成するMustUC-10
F-022備品割当確定・修正EQUIPMENT_EDIT保持者が割当案を確認し、確定または個別修正するMustUC-10
F-023貸出セッション開始(団体QR読み取り)学祭委員がオンラインのFZ-N1で団体QRコードを読み取り、その団体向けの貸出セッションを開始する。開始時に団体の確定済み割当リストを端末へ事前ダウンロードし、セッション中の照合に使用するMustUC-11
F-024貸出対象照合担当者がスキャンした備品が、開始済みセッションの団体へ割り当てられた備品かどうかをオンラインではサーバー、オフラインでは端末へ事前保存した割当リストで即時判定し、指定外の場合は警告する。オンライン復帰後はサーバーでも再検証するMustUC-11
F-025貸出・配置確認スキャン記録貸出時スキャンと持出後の二重確認スキャンについて、割当の一致にかかわらず、団体、備品、日時、実際のスキャン担当の区別、認証上の操作元、端末識別子、判定結果を記録し、貸出時は貸出セッションも紐付けるMustUC-11
F-026年度更新リセット備品の状態、現在地、企画割当を初期化し、進行中の貸出セッションを終了扱いとして履歴をアーカイブするShouldUC-12
F-027備品状態の強制変更理由の入力を必須として備品情報を強制変更するCouldUC-13
F-028権限確認学祭委員の権限を一覧またはNFCスキャンで確認するCouldUC-13
F-029外部エクスポートAPIAPIキー認証により全備品データを返すCouldUC-14
F-030全項目変更履歴備品に紐づく全項目の変更前後の値と操作情報を記録するMustUC-15
F-031履歴閲覧Sho-Roomで備品単位の変更履歴とスキャン履歴を時系列表示するMustUC-15
F-032権限管理roleConfig.tsのDiscordロールID別設定から権限ビットを計算し、各機能が要求するビットでアクセスを制御するMust全UC共通
F-033認証・セッション基盤利用Scope Out(共通認証基盤のスコープ)。login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等が担うOAuth、Cookie、JWTの発行・検証等の認証・セッション機構を利用し、本システムでは新規設計・実装しないScope OutUC-16
F-034BFF API中継Sho-Room Workerの既存BFFを利用して、ブラウザからの同一オリジン要求を備品管理APIへ中継する。BFFはCookieからJWTを抽出してAuthorizationヘッダーへ変換し、新規の認証・セッション方式は定義しないMustUC-17
F-035Web CSRF対策Scope Out(共通認証基盤・既存Web/BFFのスコープ)。login-api等の共通認証基盤と既存Web/BFFのCSRF対策を利用し、本システムでは要件を定義しないScope OutUC-16
F-036Webセッション失効Scope Out(共通認証基盤のスコープ)。login-api等の共通認証基盤が担うセッション失効・Cookie削除の機構を利用し、本システムでは要件を定義しないScope OutUC-16
F-037企画マスタ管理参加団体に紐づく企画を登録、編集、無効化するMustUC-07
F-040貸出セッション管理FZ-N1上の貸出セッションの開始(団体QRスキャン)・終了(担当者からの端末返却または学祭委員による明示的終了)を管理するMustUC-11

F-038とF-039は、旧仕様で定義していた参加団体向けセルフスキャン関連機能をスコープ外化したため欠番とする。F-040は、その後に追加した学祭委員立会い方式の貸出セッション管理に割り当てている。


2. アーキテクチャ構成​

2.1 技術スタック​

バックエンド​

  • Platform: Cloudflare Workers (Hono)

  • Database: Cloudflare D1 (2 Database 構成)

    • Equipment DB (soshosai-equipment-db): 備品、場所、履歴、ログを管理

    • System DB (soshosai-system-db): 実行委員(スタッフ)情報を管理。認証時に参照

  • Web認証・セッション: 共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)を利用する。login-apiがOAuthフローとCookieの設定・削除、discord-auth-worker等がJWT発行、unified-auth-workerがJWT検証・BAN管理を担う。Sho-Room Workerは既存BFFとしてCookieからJWTを抽出し、Authorizationヘッダーへ変換して備品管理APIへ中継する。本システムでは認証・セッション基盤を新規設計・管理しない

現行のSho-Room Workerでは、共通認証基盤が設定したsoshosai_staff_jwt Cookieから、packages/web/sho-room/worker/utils.tsのextractJwtでJWTを取り出す。BFFは取得したJWTをAuthorization: Bearer ...ヘッダーへ設定して下流APIへ中継し、JWTの署名・権限検証はunified-auth-workerが担う。

FZ-N1の貸出セッションは認証セッションではなく、団体、開始・終了日時、開始した学祭委員、端末を記録する業務データとして扱う。

  • ORM: Drizzle ORM(または同等のTypeScript対応ORM)による型安全なデータベース操作

  • バリデーション: Zod / drizzle-orm/zod による型の正確なバリデーション

フロントエンド​

  • Handy (Main): Native Android (Java) - Panasonic FZ-N1専用アプリ

    • Android 6.0.1 (API Level 23)

    • minSdk = 23

  • Web (Sub): React (TypeScript) / Sho-Roomモジュール(レスポンシブデザイン)

    • デプロイ: Cloudflare Workers + Static Assets

    • BFF (Backend for Frontend): Cloudflare Workers(同一Worker内でAPIプロキシを実装)

    • テスト: Vitest

  • Printer: Epson TM-L90(PC 直結、ラベル印刷エージェント経由)

2.2 ドメイン構成​

本システムは以下のドメイン構成で運用される:

  • login-api: loginapi.soshosai.com - 認証フロントエンド

  • auth-workers: 認証バックエンドサービス群(Service Binding経由)

    • discord-auth-worker: Discord OAuth2認証

    • line-auth-worker: LINE OAuth2認証

    • unified-auth-worker: JWT検証・BAN管理

  • Sho-Room: room.soshosai.com - 学祭委員向け管理画面(本システム)

  • 来場者アプリ: app.soshosai.com - 来場者向けアプリ(別システム)

  • 共通API: api.soshosai.com - バックエンドAPI(共通)

  • 申請システム: 企画ごと・備品種別ごとの必要数を提供する外部システム

2.3 認証アーキテクチャ​

認証基盤の新規構築は本システムの対象外とし、login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等の既存基盤を利用する。OAuthフローとCookieの設定・削除はlogin-api、JWT発行はdiscord-auth-worker等、JWT検証・BAN管理はunified-auth-workerが担う。Webブラウザでは、Sho-Room Workerが既存BFFとしてCookieからJWTを取り出し、Authorizationヘッダーへ変換して備品管理APIへ中継する。本システムでは認証・セッション基盤を新規設計しない。FZ-N1等のネイティブクライアントは既存認証基盤のJWTで備品管理APIを利用する。参加団体の担当者は、学祭委員がログインしたFZ-N1を一時的に操作するだけで、別の認証を行わない。

認証システムの詳細は認証システム仕様書を参照

概要:

コンポーネント役割
login-apiOAuthフロー、Cookie設定・削除
discord-auth-workerDiscord OAuth2認証、ロール検証、JWT発行
line-auth-workerLINE OAuth2認証、JWT発行
unified-auth-workerJWT検証・BAN管理・レート制限

BFFパターン:

Sho-Room Worker(Cloudflare Workers、Static Assets + 同一WorkerによるBFF)の既存BFFを利用し、ブラウザからの同一オリジン要求を備品管理APIへ中継する。現行実装では、共通認証基盤が設定したsoshosai_staff_jwt Cookieから、packages/web/sho-room/worker/utils.tsのextractJwtがJWTを取得する。BFFはCookieを下流へ転送せず、Authorization: Bearer ...ヘッダーを付与してAPIへ中継する。JWTの署名・権限検証はunified-auth-workerが担う。本システムはこの既存BFFを利用するが、共通認証基盤の認証・セッション詳細を新規定義しない。

参加団体向けのCookieセッションやBFF経由の貸出操作は提供しない。貸出時の備品スキャンは、開始済みの貸出セッションに紐付く団体IDをサーバー側で解決し、ブラウザから団体や企画を指定させない。

Browser → Workers (Static Assets: React) → Workers (BFF) → API → unified-auth-worker
Cookie Header変換

2.4 ID戦略​

  • システム内部ID: 新規エンティティの主キーは原則 UUID v7 を使用する。ただし、既存API互換のためItemHistory.id、SyncLog.id、AuthorityChangeLog.idはINTEGER PRIMARY KEY AUTOINCREMENTを維持する

  • 操作ログの記録者ID: 学祭委員の操作は Discord User ID、貸出時の備品スキャンは 貸出セッションIDと対象団体ID、持出後の二重確認スキャンは 配置確認の操作元と端末識別子 を主体として記録し、開始した学祭委員IDを追跡できるようにする

  • Web認証・セッション: 共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)の既存機構を利用する。Sho-Room Workerは既存BFFとしてCookieからJWTを抽出してAPIへ中継し、本システムではID形式・保存方法・有効期限等を定義しない

  • 貸出セッションID: UUID v7を使用し、対象団体、開始・終了日時、開始・終了操作元、端末識別子を記録する

2.5 データフロー​

  • Items(備品): Equipment DBがマスター(Single Source of Truth)。Google SpreadsheetとのExport / Importは行わない

  • Projects / EquipmentRequests: 企画と申請システムから取得した必要数をEquipment DBで管理する

  • Allocations: 講義棟出展分の除外と同一教室優先を反映した割当をEquipment DBで管理する

  • Staffs(名簿): 外部システムによりsoshosai-system-dbへデータが供給される前提。本システムは登録・管理を行わず、認証照合と権限参照に利用する

  • Locations / Logs: Equipment DB内で管理する。変更、スキャン、ラベル、通知、同期の監査情報は無期限に保持する

  • 認証・セッション / LendingSession: Webの認証・セッションは共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)の既存機構を利用し、Sho-Room WorkerはCookieからJWTを抽出してAPIへ中継する既存BFFの役割を担う。認証・セッション情報はEquipment DBでは管理しない。LendingSessionはFZ-N1で行う団体貸出の業務記録としてEquipment DB(Cloudflare D1)で管理する。貸出時スキャンの団体IDは貸出セッションから取得し、参加者やブラウザからの入力を信頼しない

2.6 システム構成図​


3. データベース設計​

本システムは2つのD1データベースを使用する。

詳細は各DB仕様書を参照

3.1 Equipment DB (soshosai-equipment-db)​

備品管理専用のデータベース。

テーブル用途
Locations場所マスター
Organizations使用団体マスター
Projects参加企画マスター
ItemTypes備品種別マスター
Items備品マスター
EquipmentRequests申請システムから取得した必要数
EquipmentRequestImportLog申請数取込の履歴
Allocations備品割当
ItemHistory操作ログ
ScanLog個別・貸出時・持出後の二重確認・返却時スキャン履歴
LabelPrintLogラベル発行履歴
LabelAttachmentCheckLog貼付後確認履歴
OfflineOperationオフライン操作と競合
LoginNotificationLogDiscordログイン通知の実行状態
LendingSessionFZ-N1上の団体貸出セッション
SyncLog同期ログ
AuthorityChangeLog権限変更ログ

詳細: 備品DB設計

共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)が管理する認証・セッション関連データは、Equipment DBのテーブルとして定義しない。Sho-Room WorkerのBFFはCookieからJWTを抽出して備品管理APIへ中継するが、認証情報そのものをEquipment DBで管理しない。

3.2 System DB (soshosai-system-db)​

システム共通のデータベース。本システムからスタッフ名簿を登録・管理せず、認証照合と権限参照のために読み取る。

テーブル用途
Staffsスタッフ情報(DB列: DiscordId, StaffName, StudentId, NfcIdm, Authority。既存認証基盤が保持するNFC情報を含む場合がある)

詳細: システムDB設計

Staffsの物理列名はDiscordId、StudentId、NfcIdm、AuthorityなどのPascalCaseです。API入出力のdiscord_id、student_idなどや、JWTペイロードのsub、permissionsとは別の名前として扱います。


4. 非機能要件​

4.1 非機能要件一覧​

区分要件
性能2,000件以上の備品を扱える。備品一覧は初期表示で全件取得せず、サーバー側検索、絞り込み、ソート、ページネーションを用いる
応答性スキャン結果の一致・不一致を、通常の通信環境で操作を妨げない時間内に表示する。具体値は性能試験計画で定める
同時アクセス最大30人程度の同時接続を想定する
可用性Cloudflareのインフラを利用し、学祭当日を含め常時稼働を前提とする
セキュリティ(Web認証)共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)が既存のOAuth、Cookie、JWT処理を担い、Sho-Room WorkerはCookieからJWTを抽出してAuthorizationヘッダーへ変換する既存BFFを利用する。本システムでは認証・セッションの詳細要件を新規定義しない。参加団体向けWeb認証は提供しない
セキュリティ(API認証)BFF、FZ-N1等から備品管理APIへの要求はJWT認証を必須とし、既存の共通認証基盤で検証する
セキュリティ(XSS)Sho-Room Reactフロントエンドにおいて、CSP、出力エスケープ、依存関係の脆弱性管理を適用する。Cookie自体の保護(HttpOnly等)は共通認証基盤の管轄だが、フロントエンドコードでのXSS対策は本システムのScope Inとする
セキュリティ(認可)Discordロールから計算される権限ビットにより、機能単位でアクセスを制御する
セキュリティ(特殊API)大学環境の外部連携APIは専用APIキーで認証する
セキュリティ(貸出セッション)FZ-N1のログイン済み状態でオンラインの貸出セッションを開始し、団体QRの平文UUIDから団体IDを解決する。セッション開始時に当年度の確定済み割当リストを端末へ事前配布し、参加団体の担当者に認証情報を渡さない。セッション中は貸出スキャン画面以外への遷移とログアウトを無効化する
プライバシーログイン通知スレッドとスレッド名へ学籍番号、カード情報、JWT等を含めない。団体QRの発行・配布情報を本システムで保持しない
監査性すべての備品変更、貸出時スキャン、持出後の二重確認スキャンについて、認証上の操作元または貸出セッション、実際のスキャン担当区分、団体、発生日時、記録日時、操作元、端末識別子を追跡できる
データ管理バックアップとリストアはCloudflareの機能を利用する。監査ログを含むデータ保持期間は無制限とする
操作性FZ-N1上では文字入力を原則不要とし、物理ボタンと選択式操作で主要フローを完結させる
運用・保守性バックエンド、Sho-Room、FZ-N1のエラーを記録し、外部ログシステムへ送信する
互換性FZ-N1はAndroid 6.0.1(API Level 23)に対応する。Sho-RoomはPC・モバイルに対応する
コストCloudflare Workers、D1、Pagesの利用範囲内で運用する

共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)が担うCookie属性、JWTの保管方法、CSRF・XSS対策、有効期限、失効、ローテーション等は既存基盤のスコープであり、本システムの非機能要件には含めない。Sho-Room Workerの既存BFFによるCookieからJWTへの変換と備品管理APIへの中継はScope Inとする。

4.2 権限ビット​

JWTのpermissionsフィールドに格納されたビットフラグで権限を管理する。値はDiscordロールから自動計算し、複数ロールの権限はORで合成する。

操作必要な権限ビット
備品一覧・詳細閲覧EQUIPMENT_VIEW(128)
備品登録・編集、備品状態の強制変更EQUIPMENT_EDIT(256)
備品削除EQUIPMENT_DELETE(512)
JWT認証を用いる備品連携処理EQUIPMENT_INTEGRATION(1024)
備品リセットEQUIPMENT_RESET(2048)
団体管理EQUIPMENT_MANAGE_ORGANIZATION(4096)
場所管理EQUIPMENT_MANAGE_LOCATION(8192)
備品種別・企画管理EQUIPMENT_MANAGE_TYPE(16384)
備品一括操作EQUIPMENT_BULK_OPERATION(32768)
権限確認・権限変更監査・通知確認・貸出セッションの強制終了SYSTEM_MANAGE(64)
全操作ALL_PERMISSIONS(全ビットOR)

EQUIPMENT_INTEGRATIONはJWT認証を用いる備品連携処理のための権限ビットであり、Google SpreadsheetとのImport / Exportには用いない。現行の大学環境cronサーバー向けエクスポートAPIは専用APIキーで認証し、この権限ビットを検査しない。

4.3 認証・セッション境界​

  • 備品管理APIはJWTによる認証を必須とし、FZ-N1は既存認証基盤が発行したJWTを使用する

  • Sho-Roomのブラウザは、共通認証基盤(login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等)が提供する既存のOAuth、Cookie、JWT発行・検証を利用する。現行実装では、login-apiが設定したsoshosai_staff_jwt Cookieから、Sho-Room Workerのpackages/web/sho-room/worker/utils.tsにあるextractJwtでJWTを取得する

  • Sho-Room Workerの既存BFFはCookieから取得したJWTをAuthorization: Bearer ...ヘッダーへ変換し、備品管理APIへ中継する。本システムではこの既存BFF中継を利用する一方、Cookieやセッションの仕組みを新規実装しない

  • Cookie属性、セッションストア、絶対有効期限、無操作有効期限、CSRF・XSS対策、失効、ローテーション等は共通認証基盤(login-api等)のスコープであり、本システムでは要件を定義しない

  • 貸出セッションは認証境界ではなく、ログイン済みFZ-N1で実行する団体貸出の業務記録として扱う

  • 貸出セッションは端末返却または学祭委員の明示操作で終了し、年度更新時に進行中のものを終了扱いにする

  • 貸出セッションは開始から設定値(初期値120分)を超えるとSESSION_TIMEOUTで自動終了し、終了漏れによる端末・団体のロックを残さない

4.4 データ管理・運用​

  • バックアップとリストアにはCloudflareの機能を利用する

  • 学祭準備期間は、大学環境のcronサーバーが専用API(GET /system/export/all)から全備品データを取得できる

  • 外部エクスポートAPIはJWTではなく専用APIキー(X-API-Key)で認証する

  • 大学環境向けエクスポートはバックアップの代替とせず、呼出頻度、保存先、保存期間は運用設計で定める

  • 監査ログ、スキャン履歴、通知履歴、ラベル発行・貼付確認履歴は無期限に保持する

  • 貸出セッション開始時に取得した割当リストと、オフライン中の照合結果は端末へ保存し、オンライン復帰後にPOST /items/syncで同期する

4.5 返却時の状態管理・監査ログ(D-02〜D-05)​

テント等のレンタル業者(スパイス)との回収タイミング交渉結果により、返却運用は次の2パターンのいずれかに確定する。D-02〜D-05は交渉結果待ちであり、現時点では最終決定を保留する。参加団体による返却時のセルフスキャンは行わない。

項目パターンA:スパイスが回収を遅らせてくれる場合パターンB:スパイスが例年通り回収する場合
D-02(返却成立条件)片付けの日(2026/10/12)に講義棟前へ返却された時点を返却成立とする昨年通り、講義棟前に備品が返却された時点を返却成立とする。ただし、この運用が学生課等から承認されるかは未確定
D-03(2日目夜の記録)記録しない。2日目夜はスキャン・記録を行わない講義棟前到着時に全数スキャンを行う。到着時点で備品への責任が備品課に移るため
D-04(中間集積場所を現在地として記録するか)記録しない。講義棟前を中間集積場所としてcurrent_location_idに記録しない混乱防止のため、講義棟前を現在地として記録する
D-05(足拭き・教室収容)備品課・学祭委員が講義棟前へ到着次第、逐次スキャン・足拭き・教室収容を行う。可能であれば、備品を持ってきた各団体自身に足拭き・教室搬入まで行ってもらう運用を志向する備品課・学祭委員がテント等の片付け後に足拭きを行って教室へ収容し、その後に全数スキャンを行う

返却成立の時点と中間集積場所の現在地記録は別に扱う。パターンAではD-03の夜間記録を作成せず、D-04の講義棟前も現在地として記録しない。パターンBでは講義棟前到着時の全数スキャンをScanLogへ保存し、D-04の現在地を講義棟前として記録する。D-05で行う逐次スキャンまたは全数スキャン、足拭き、教室収容の結果は、実際の運用に応じてScanLog、ItemHistory、Items.current_location_idへ反映する。

4.6 貸出状態の遷移(D-07確定)​

以下を実装・テストの基準となる貸出状態の遷移表とする。D-02〜D-05(返却運用のパターンA/B)とは独立したレイヤーの決定事項であり、それらの結論を待たずに正式採用する。

現在の状態操作・条件次の状態記録・制約
STOCK割当確定ALLOCATEDAllocations.status=CONFIRMEDとItemHistoryを記録
ALLOCATED通常の貸出セッションで割当一致LENDING_SCANNEDScanLog.match_result=MATCHEDを記録。セッション終了前はLENTにしない
LENDING_SCANNED貸出セッション終了LENT一致したスキャン済み備品だけを確定。未スキャンの備品はALLOCATEDのまま
ALLOCATEDEQUIPMENT_BULK_OPERATIONによる理由付き例外処理LENT通常フローでは禁止し、理由と権限を監査記録へ保存
LENT返却成立RETURNEDD-02〜D-05の採用パターンに従い、返却スキャンとItemHistoryを記録
RETURNED足拭き・教室収容完了STOCK実際の保管場所をcurrent_location_idへ反映

持出後の二重確認スキャンは貸出状態を直接変更せず、source=placement-confirmationのScanLogと必要なItemHistoryを記録する。condition(NORMAL、NEEDS_REPAIR、UNDER_REPAIR、BROKEN、LOST、DISPOSED)は貸出状態とは独立して更新する。EQUIPMENT_EDITによる強制変更は上表以外の遷移も許可できるが、理由を必須とし、通常フローの代替にはしない。

上表の遷移は、それぞれ対応する専用の操作経路でのみ行う。汎用の更新API(PUT /items/:id)はstatusを受け付けず、遷移表の記録要件を迂回できないようにする。詳細は§5.7のPUT /items/:idを参照。


5. API仕様​

5.1 共通仕様​

5.1.1 認証​

  • 備品操作・管理APIはJWTによる認証を必須とする。NFCログイン、共通認証基盤(login-api等)によるWeb認証、Sho-Room Workerの既存BFF中継、大学環境向けエクスポートなどは、それぞれの既存または専用の認証方式を使用する

  • JWT認証を行うエンドポイントではAuthorization: Bearer <JWT>ヘッダーでトークンを送信する

  • JWTペイロードでは操作者のDiscord User IDをsub、権限ビットをpermissionsとして表現する(操作ログ記録用)。これはSystem DBのStaffs.DiscordId、Staffs.Authorityとは別のフィールド名であり、APIのdiscord_id、student_idなどの入出力フィールドとも区別する

  • FZ-N1等のネイティブクライアントは既存認証基盤が発行したJWTを使用する

  • Sho-Roomのブラウザは共通認証基盤(login-api、discord-auth-worker、unified-auth-worker等)の既存認証・セッション機構を使用する。参加団体向けブラウザ画面は提供しない

  • 既存のSho-Room Worker BFFはCookieからJWTを取得し、API要求にAuthorization: Bearer ...ヘッダーを付与して中継する。JWTの検証はunified-auth-workerが行う

  • Cookie、JWT、リフレッシュトークン等の機密値をログやエラーレスポンスへ出力しない

5.1.2 レスポンス形式​

#755で決定(2026-09-09): リポジトリ標準のshared/utils/response.ts(ok/badRequest/unauthorized/forbidden/notFound/conflict/serverError)にレスポンス形式を合わせる。備品管理APIだけの専用形式は持たない(旧仕様の{status, code, message}/{status, data}は採用しない)。

理由: {status, code, message}系の形式を定義していたのは備品管理の仕様書のみで、sharedのレスポンスヘルパーは他12 worker(system-workers 9本・auth-workers 3本)で既に使われている。フロント(sho-room)も{error}前提で実装済み。備品管理側だけ別形式にすると、他メンバー担当分を含む既存実装・フロントの両方に波及するコストの方が大きい一方、shared側は元々badRequest/unauthorized/forbiddenがdetails引数を持っており(notFound/conflictにも#755で追加)、業務ロジック上意味のある機械可読コードを捨てずに済む。

成功時: ラッパーなし。データを裸で返す(ok(c, data) → c.json(data, status))。

{ "id": "...", "...": "..." }

エラー時:

{
"error": "エラーメッセージ(画面表示用の日本語)",
"details": { "code": "VALIDATION_ERROR", "...": "..." }
}
  • errorは画面表示用メッセージ、detailsは省略可能なフィールドで、機械可読のcodeや追加情報を任意に載せる
  • codeはクライアントが文字列比較で分岐する際に使う機械可読識別子とし、errorの文字列比較では分岐しない。主な値はVALIDATION_ERROR、UNAUTHORIZED、FORBIDDEN、NOT_FOUND、STAFF_NOT_FOUND、CONFLICT、NOT_IN_MEMBER_LIST、NFC_IDM_MISMATCH、ACTIVE_LENDING_SESSION_EXISTS、ALLOCATION_NOT_CONFIRMED、ITEM_STATE_CONFLICT、DUPLICATE_OPERATION、SYNC_CONFLICT、PRINTER_UNAVAILABLE、TOO_MANY_REQUESTS、INTERNAL_SERVER_ERRORとする
  • codeを返さないエンドポイント(details省略)も許容する。フロントはerror ?? messageのフォールバックを既に持つため、codeが無くても画面表示は壊れない

5.1.3 HTTPステータスコード​

enum HttpStatus {
// 成功系
OK = 200, // 成功
CREATED = 201, // 作成成功
ACCEPTED = 202, // 受理
NO_CONTENT = 204, // コンテンツなし(削除成功など)

// リダイレクト系
MOVED_PERMANENTLY = 301, // 恒久的に移動
FOUND = 302, // 発見
NOT_MODIFIED = 304, // 未更新

// クライアントエラー系
BAD_REQUEST = 400, // リクエスト不正
UNAUTHORIZED = 401, // 認証失敗
FORBIDDEN = 403, // 権限不足
NOT_FOUND = 404, // リソースが見つからない
METHOD_NOT_ALLOWED = 405, // 許可されていないメソッド
NOT_ACCEPTABLE = 406, // 受理できない
CONFLICT = 409, // 競合
GONE = 410, // 消滅
PAYLOAD_TOO_LARGE = 413, // ペイロードが大きすぎる
UNSUPPORTED_MEDIA_TYPE = 415, // サポートされていないメディアタイプ
UNPROCESSABLE_ENTITY = 422, // 処理できないエンティティ(バリデーションエラー)
TOO_MANY_REQUESTS = 429, // リクエスト過多

// サーバーエラー系
INTERNAL_SERVER_ERROR = 500, // サーバーエラー
NOT_IMPLEMENTED = 501, // 未実装
BAD_GATEWAY = 502, // 不正なゲートウェイ
SERVICE_UNAVAILABLE = 503, // サービス利用不可
GATEWAY_TIMEOUT = 504, // ゲートウェイタイムアウト
}

主な使用例:

  • 200 OK: 取得・更新成功

  • 201 CREATED: 作成成功

  • 204 NO_CONTENT: 削除成功

  • 400 BAD_REQUEST: リクエスト形式エラー

  • 401 UNAUTHORIZED: 認証失敗、JWTなし/無効

  • 403 FORBIDDEN: 権限不足

  • 404 NOT_FOUND: リソースが見つからない

  • 409 CONFLICT: データ競合(同一操作の重複など)

  • 422 UNPROCESSABLE_ENTITY: バリデーションエラー

  • 500 INTERNAL_SERVER_ERROR: サーバー内部エラー

5.1.4 ページネーション​

  • Web UI: start(開始位置)、end(終了位置)パラメータで範囲指定

  • FZ-N1: 50件ごと、start、endパラメータで範囲指定

5.2 認証API(Auth)​

POST /auth/nfc​

  • 概要: FZ-N1用。NFCタッチによるログイン

  • 対象: FZ-N1アプリ

  • リクエストボディ:

{
"student_id": "s1234567",
"idm": "0123456789ABCDEF"
}
  • 処理ロジック:

    1. 受信したidmを大文字・区切り文字なしの16進表記へ正規化する

    2. System DBのStaffsテーブルをStudentIdで検索する

    3. レコードなし → 403 Forbidden(code: NOT_IN_MEMBER_LIST)

    4. レコードが存在しても、Staffs.NfcIdmが未登録または正規化後のidmと一致しない場合は403 Forbidden(code: NFC_IDM_MISMATCH)。学籍番号だけでのフォールバック照合は行わない

    5. 権限の取得・確認に失敗 → 403 Forbidden(code: FORBIDDEN)

    6. 学籍番号とIDmが一致 → 200 OK + JWT発行(subにDiscord User ID、permissionsに権限ビットを設定)

    7. ログイン成功後、Discord通知処理を非同期に開始する。通知失敗はログイン結果に影響させない

idm照合を行わない場合は他人の学生証で学籍番号を偽装できるため、本システムではIDm照合を必須とする。NfcIdmの登録・再登録は既存認証基盤または名簿供給元の責務であり、未登録カードは名簿管理担当者による登録完了までログインできない。

  • レスポンス例:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"discord_id": "123456789012345678",
"student_id": "s1234567",
"permissions": 0
}
}

GET /auth/discord-callback、POST /api/logout​

  • 扱い: 対象外(共通認証基盤の既存認証フロー)

  • 概要: Sho-Room (Web)用のDiscord OAuthコールバックとログアウト。詳細は認証システム仕様書を参照

  • 対象: Webアプリ

  • 本システムの扱い: 本システムではコールバック、Cookie発行・削除、セッション作成・失効の詳細を定義・実装しない。FZ-N1のログアウトは端末内のJWTと未送信データを処理する

5.2.1 Discordログイン通知(F-002 / UC-02)​

POST /auth/nfcでログインが成立した後、通知処理をログイン処理から分離して実行する。

  • 設定されたDiscordテキストチャンネル配下へ、ログイン単位のプライベートスレッドを作成する

  • Botがログインしたスタッフをスレッドメンバーへ追加し、allowed_mentionsで対象者だけを許可したメンション付きメッセージを投稿する

  • スレッドは招待された本人と、Discord上でMANAGE_THREADS権限を持つユーザーが閲覧できる。DM通知は行わない

  • 通知にはログイン日時、端末識別子、ログイン結果を含める。学籍番号、カード情報、JWT等の機密情報は記載しない

  • スレッド名には氏名、学籍番号、Discord名等を含めず、ランダムなログイン識別子を使用する

  • スレッドID、対象Discord ID、ログイン日時、スレッド作成日時、メッセージ送信日時、削除予定日時、通知結果、削除結果をLoginNotificationLogへ記録する

  • 削除予定日時はメッセージ送信日時の15分後とし、その時刻に通知スレッドを削除する。投稿に失敗した場合はスレッド作成日時の15分後とする

  • 削除判定を1分間隔で実行し、削除予定日時を経過したスレッドをDiscord APIで削除する。通常時は送信から15分以上16分未満で削除する

  • 削除失敗時は再試行し、SYSTEM_MANAGE保持者が失敗を確認できるようにする。Discordの自動アーカイブは削除の代替としない

  • Discord APIの障害や権限不足で通知できない場合もログインは成立させ、通知失敗を監査ログへ記録する

  • Discordアプリの通知設定によるプッシュ通知の表示までは保証せず、スレッドへの投稿成功までを保証範囲とする

5.3 スタッフ情報API(Staffs)​

GET /staffs/me​

  • 概要: 現在ログイン中のスタッフ情報を取得

  • 権限: JWT認証済み(追加の備品権限ビットは不要)

  • レスポンス例:

{
"discord_id": "123456789012345678",
"student_id": "s1234567",
"staff_name": "山田 太郎",
"permissions": 0
}

PUT /staffs/me​

  • 扱い: 対象外。学祭委員名簿(Staffs)は外部システムから供給されるため、本システムから更新しない

  • レスポンス: 405 METHOD_NOT_ALLOWED

  • 備考: スタッフ情報の変更は外部システムまたは既存の認証基盤で行う

5.4 場所管理API(Locations)​

GET /locations​

  • 概要: 全場所リストの取得

  • 権限: EQUIPMENT_VIEW

  • レスポンス例:

{
"locations": [
{
"id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"name": "M1",
"created_at": "2024-01-15T10:00:00",
"updated_at": "2024-01-15T10:00:00"
},
{
"id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"name": "講義棟",
"created_at": "2024-01-15T10:01:00",
"updated_at": "2024-01-15T10:01:00"
}
]
}

POST /locations​

  • 概要: 新規場所の登録

  • 権限: EQUIPMENT_MANAGE_LOCATION

  • リクエストボディ:

{
"name": "体育館"
}
  • 処理: idにはUUID v7を自動生成

  • バリデーション:

    • name: 必須、1-50文字、空白のみ不可
  • レスポンス例:

{
"id": "018d3f7a-7e6g-9d4c-bf3h-5ga8e6d4h3c",
"name": "体育館",
"created_at": "2024-01-26T14:30:00",
"updated_at": "2024-01-26T14:30:00"
}

PUT /locations/:id​

  • 概要: 場所情報の更新

  • 権限: EQUIPMENT_MANAGE_LOCATION

  • パスパラメータ: id (UUID)

  • リクエストボディ:

{
"name": "新体育館"
}
  • バリデーション: POST /locationsと同様

  • レスポンス: 更新後の場所情報

DELETE /locations/:id​

  • 概要: 場所の削除

  • 権限: EQUIPMENT_MANAGE_LOCATION

  • パスパラメータ: id (UUID)

  • 処理: 参照の有無にかかわらず物理削除は行わず、is_activeを0に設定して無効化する。参照中でも削除処理は成立させ、204 No Contentを返す

  • レスポンス:

(本文なし。204 No Content)

5.5 団体管理API(Organizations)​

GET /organizations​

  • 概要: 全団体リストの取得

  • 権限: EQUIPMENT_VIEW

  • レスポンス例:

{
"organizations": [
{
"id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"name": "展示ブース A",
"created_at": "2024-01-15T10:00:00",
"updated_at": "2024-01-15T10:00:00"
}
]
}

POST /organizations​

  • 概要: 新規団体の登録

  • 権限: EQUIPMENT_MANAGE_ORGANIZATION

  • リクエストボディ:

{
"name": "展示ブース B"
}
  • バリデーション:

    • name: 必須、1-100文字、空白のみ不可

PUT /organizations/:id​

  • 概要: 団体情報の更新

  • 権限: EQUIPMENT_MANAGE_ORGANIZATION

DELETE /organizations/:id​

  • 概要: 団体の削除

  • 権限: EQUIPMENT_MANAGE_ORGANIZATION

  • 処理: 参照の有無にかかわらず物理削除は行わず、is_activeを0に設定して無効化する。参照中でも削除処理は成立させ、204 No Contentを返す

5.6 備品種別管理API(ItemTypes)​

GET /item-types​

  • 概要: 全備品種別リストの取得

  • 権限: EQUIPMENT_VIEW

  • レスポンス例:

{
"item_types": [
{
"id": "018d3f7a-9g8i-bf6e-dh5j-7ic0g8f6j5e",
"name": "長机",
"created_at": "2024-01-15T10:00:00",
"updated_at": "2024-01-15T10:00:00"
},
{
"id": "018d3f7a-ah9j-cg7f-ei6k-8jd1h9g7k6f",
"name": "椅子",
"created_at": "2024-01-15T10:01:00",
"updated_at": "2024-01-15T10:01:00"
}
]
}

POST /item-types​

  • 概要: 新規備品種別の登録

  • 権限: EQUIPMENT_MANAGE_TYPE

  • リクエストボディ:

{
"name": "プロジェクター"
}
  • バリデーション:

    • name: 必須、1-50文字、空白のみ不可

PUT /item-types/:id​

  • 概要: 備品種別情報の更新

  • 権限: EQUIPMENT_MANAGE_TYPE

DELETE /item-types/:id​

  • 概要: 備品種別の削除

  • 権限: EQUIPMENT_MANAGE_TYPE

  • 処理: 参照の有無にかかわらず物理削除は行わず、is_activeを0に設定して無効化する。参照中でも削除処理は成立させ、204 No Contentを返す

5.7 備品操作・検索API(Items)​

GET /items​

  • 概要: 多機能検索とページネーション

  • 権限: EQUIPMENT_VIEW

  • クエリパラメータ:

    • start (integer, optional): 開始位置(デフォルト: 0)

    • end (integer, optional): 終了位置(デフォルト: 50)

    • q (string, optional): フリーワード検索(display_id、item_type_name、organization_name、project_nameへの部分一致)

    • status[] (string[], optional): 貸出状態フィルタ(複数指定可: STOCK, ALLOCATED, LENDING_SCANNED, LENT, RETURNED)

    • condition[] (string[], optional): コンディションフィルタ(複数指定可)

    • current_location_id[] (string[], optional): 現在地フィルタ(複数指定可)

    • item_type_id[] (string[], optional): 種別フィルタ(複数指定可)

    • organization_id[] (string[], optional): 団体フィルタ(複数指定可)

    • project_id[] (string[], optional): 企画フィルタ(複数指定可)

    • destination_id[] (string[], optional): 持出先フィルタ(複数指定可)

    • is_active (boolean, optional): trueで有効な備品だけ、falseで論理削除済みだけを返す(省略時はtrue)

    • sort_by (string, optional): ソート項目(display_id/created_at/updated_at、デフォルト: display_id)

    • sort_order (string, optional): ソート順(asc/desc、デフォルト: asc)

  • レスポンス例:

{
"items": [
{
"id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"display_id": "DESK-001",
"item_type_id": "018d3f7a-9g8i-bf6e-dh5j-7ic0g8f6j5e",
"item_type_name": "長机",
"status": "LENT",
"condition": "NORMAL",
"original_location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"original_location_name": "M1",
"current_location_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"current_location_name": "講義棟",
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"organization_name": "展示ブース A",
"project_id": "018d3f7a-project-0000-0000-000000000001",
"project_name": "展示企画 A",
"destination_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"destination_name": "講義棟",
"note": "脚部に傷あり",
"revision": 13,
"is_active": 1,
"created_at": "2024-01-15T10:00:00",
"updated_at": "2024-01-26T09:00:00"
}
],
"meta": {
"total_count": 1523,
"start": 0,
"end": 50
}
}

GET /items/:id​

  • 概要: 備品詳細情報の取得

  • 権限: EQUIPMENT_VIEW

  • パスパラメータ: id (UUID)

  • レスポンス例:

{
"id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"display_id": "DESK-001",
"item_type_id": "018d3f7a-9g8i-bf6e-dh5j-7ic0g8f6j5e",
"item_type_name": "長机",
"status": "LENT",
"condition": "NORMAL",
"original_location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"original_location_name": "M1",
"current_location_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"current_location_name": "講義棟",
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"organization_name": "展示ブース A",
"project_id": "018d3f7a-project-0000-0000-000000000001",
"project_name": "展示企画 A",
"destination_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"destination_name": "講義棟",
"note": "脚部に傷あり",
"revision": 13,
"is_active": 1,
"created_at": "2024-01-15T10:00:00",
"updated_at": "2024-01-26T09:00:00"
}

POST /items​

  • 概要: 新規備品の登録

  • 権限: EQUIPMENT_EDIT

  • リクエストボディ:

{
"display_id": "DESK-002",
"item_type_id": "018d3f7a-9g8i-bf6e-dh5j-7ic0g8f6j5e",
"original_location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"note": "新品"
}
  • バリデーション:

    • display_id: 必須、1-50文字、重複不可、空白のみ不可

    • item_type_id: 必須、存在するItemTypeのIDであること

    • original_location_id: 必須、存在するLocationのIDであること

    • note: 任意、最大500文字

  • 処理: idにはUUID v7を自動生成、statusはSTOCK、conditionはNORMAL、current_location_idはoriginal_location_idと同じ値で初期化

  • レスポンス: 作成された備品情報(status: 201)

PUT /items/:id​

  • 概要: 備品情報の更新。貸出状態(status)は本エンドポイントで変更しない

  • 権限: EQUIPMENT_EDIT

  • パスパラメータ: id (UUID)

  • リクエストボディ:

{
"display_id": "DESK-002-NEW",
"item_type_id": "018d3f7a-9g8i-bf6e-dh5j-7ic0g8f6j5e",
"condition": "NEEDS_REPAIR",
"original_location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"current_location_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"organization_id": null,
"project_id": null,
"destination_id": null,
"note": "脚部修理必要"
}
  • バリデーション: POST /itemsと同様(display_id変更時は重複チェック)。project_idとdestination_idは両方を指定する場合に有効な割当先であることを確認する

  • statusを受け付けない理由: §4.6の貸出状態の遷移は、いずれも専用の操作経路と記録要件(ScanLog、Allocations、理由入力、EQUIPMENT_BULK_OPERATIONの権限確認)を持つ。本エンドポイントでstatusを任意設定できると、それらを一切伴わずに同じ最終状態を作れてしまうため、statusは受け付けない。statusが指定された場合は400 Bad Request(code: VALIDATION_ERROR)を返す

  • 貸出状態の変更経路: STOCK→ALLOCATEDはPOST /allocations/confirm、ALLOCATED→LENDING_SCANNED→LENTは団体貸出セッションAPI(§5.7.3)、ALLOCATED→LENTの例外処理はPOST /items/organization-action、LENT→RETURNEDおよびRETURNED→STOCKはPOST /items/syncによる返却記録、遷移表を外れる変更はPOST /special/force-status-change(理由必須)を使用する

  • condition: §4.6のとおり貸出状態とは独立して更新でき、遷移表の制約を受けない。Sho-Roomの編集モーダル(§8.4)が扱う項目も本エンドポイントの範囲と一致する

  • レスポンス: 更新後の備品情報

DELETE /items/:id​

  • 概要: 備品の削除(論理削除)

  • 権限: EQUIPMENT_DELETE

  • パスパラメータ: id (UUID)

  • 処理: is_activeを0に設定(論理削除)

  • レスポンス:

(本文なし。204 No Content)

POST /items/organization-action/preview​

  • 概要: 団体QRスキャン時の事前確認(一括操作プレビュー)。通常の団体貸出では使用せず、例外処理の対象を確認するために使用する

  • 権限: EQUIPMENT_BULK_OPERATION(JWT認証済みの学祭委員)。通常の団体貸出にこの権限は要求せず、貸出セッションAPIを使用する

  • リクエストボディ:

{
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d"
}
  • レスポンス例:
{
"organization": {
"id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"name": "展示ブース A"
},
"summary": [
{
"item_type_name": "長机",
"count": 4
},
{
"item_type_name": "椅子",
"count": 10
}
],
"items": [
{
"id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"display_id": "DESK-001",
"item_type_name": "長机",
"status": "LENT",
"condition": "NORMAL",
"original_location_name": "M1",
"destination_name": "講義棟",
"project_name": "展示企画 A"
},
{
"id": "018d3f7a-cj1l-ei9h-gk8m-alf3j1i9m8h",
"display_id": "CHAIR-001",
"item_type_name": "椅子",
"status": "LENT",
"condition": "BROKEN",
"original_location_name": "M2",
"destination_name": "講義棟",
"project_name": "展示企画 A"
}
]
}

POST /items/organization-action​

  • 概要: 団体一括操作の実行(一括貸出/一括返却、例外処理専用)。通常の団体貸出をこのAPIで代替してはならず、UC-11の貸出セッション方式を使用する

  • 権限: EQUIPMENT_BULK_OPERATION(JWT認証済みの学祭委員)

  • リクエストボディ:

{
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"action": "LEND",
"reason": "指定外備品の救済"
}
  • 処理ロジック:

    1. 指定された団体に属する企画のうち、確定済み割当を持つ対象備品を特定する

    2. actionがLENDの場合: 一括操作を使用する理由を必須入力として受け取り、状態遷移を検証したうえでstatusをLENTへ変更する。これは例外処理であり、通常の団体貸出のような個別スキャン・割当照合を省略するため、EQUIPMENT_BULK_OPERATIONを必須とする

    3. actionがRETURNの場合: D-02〜D-05で最終確定したパターンに従い、返却成立、スキャン、現在地、足拭き、教室収容を記録する。現時点はスパイスとの交渉結果待ちのためA/B両パターンを保持し、参加団体によるセルフスキャンは行わない

    4. 操作単位の記録に加え、対象となった各備品の変更前後をItemHistoryへ記録する(actor_idにはJWTのsub(Discord User ID)を使用)

    5. 不正な状態遷移や確定前の割当は更新せず、code: ITEM_STATE_CONFLICTまたはcode: ALLOCATION_NOT_CONFIRMEDの競合として返す

    action: LENDのリクエストではreasonを必須とし、例外処理の理由をItemHistoryへ保存する。通常の貸出でこのAPIを呼び出した場合は、権限の有無にかかわらず運用違反として扱い、FZ-N1の通常メニューからは導線を提供しない。

  • レスポンス例:

{
"updated_count": 14,
"organization_name": "展示ブース A"
}

POST /items/sync​

  • 概要: FZ-N1からのバッチ更新(個別スキャン結果の送信)

  • 権限: 貸出セッション、配置確認、返却記録、照会のみの個別スキャンはEQUIPMENT_VIEW。備品状態・備品情報を変更する個別操作はEQUIPMENT_EDIT

  • リクエストボディ:

{
"updates": [
{
"operation_id": "018d3f7a-operation-0000-0000-000000000001",
"id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"device_id": "FZ-N1-001",
"occurred_at": "2026-10-09T09:00:00+09:00",
"base_revision": 12,
"status": "RETURNED",
"current_location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"condition": "NORMAL",
"source": "return-operation",
"scanned_by_role": "STAFF",
"match_result": "RETURN_RECORDED"
},
{
"operation_id": "018d3f7a-operation-0000-0000-000000000002",
"id": "018d3f7a-cj1l-ei9h-gk8m-alf3j1i9m8h",
"device_id": "FZ-N1-001",
"occurred_at": "2026-10-09T09:00:01+09:00",
"base_revision": 7,
"status": "STOCK",
"current_location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"condition": "NEEDS_REPAIR",
"source": "individual-scan",
"scanned_by_role": "STAFF",
"match_result": "MATCHED"
},
{
"operation_id": "018d3f7a-operation-0000-0000-000000000004",
"id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"device_id": "FZ-N1-001",
"occurred_at": "2026-10-09T09:00:30+09:00",
"base_revision": 12,
"status": "LENDING_SCANNED",
"current_location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"condition": "NORMAL",
"source": "lending-session",
"scanned_by_role": "ORG_REPRESENTATIVE",
"match_result": "MATCHED",
"lending_session_id": "018d3f7a-session-0000-0000-000000000001",
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"allocated_project_id": "018d3f7a-project-0000-0000-000000000001"
},
{
"operation_id": "018d3f7a-operation-0000-0000-000000000003",
"id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"device_id": "FZ-N1-001",
"occurred_at": "2026-10-09T09:01:00+09:00",
"base_revision": 13,
"status": "LENT",
"current_location_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"condition": "NORMAL",
"source": "placement-confirmation",
"scanned_by_role": "ORG_REPRESENTATIVE",
"match_result": "PLACEMENT_CONFIRMED",
"lending_session_id": null,
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"allocated_project_id": "018d3f7a-project-0000-0000-000000000001"
}
]
}
  • 必須項目: device_id、端末での操作日時occurred_at、操作時点で端末が保持したbase_revision、source、scanned_by_roleを各更新に含める。スキャンを伴う更新ではmatch_resultも必須とする。貸出セッションのスキャンではlending_session_id、organization_id、allocated_project_idを追加し、配置確認ではlending_session_idを空にする。sourceにはindividual-scan、lending-session、placement-confirmation、return-operationのいずれかを指定し、scanned_by_roleにはSTAFFまたはORG_REPRESENTATIVEを指定する。placement-confirmationは二重確認スキャンをサーバーへ伝えるための専用値であり、専用エンドポイントは設けない。ラベル貼付後確認はPOST /items/syncではなくPOST /labels/attachment-checkで記録する

  • 処理: 各備品をbase_revisionとサーバー側の現在値で競合検証し、受理した更新をItemsへ反映してItemHistoryおよび必要なScanLogへログを記録する。source=placement-confirmationの場合は貸出セッションを再開せず、配置完了結果としてScanLogへ保存する

  • レスポンス例:

{
"success_count": 3,
"failed_count": 0,
"conflict_count": 0,
"conflicts": []
}

operation_idは端末内で一意に生成し、再送されても同じ操作を二重適用しない。サーバー側の更新が新しい場合に操作を単純破棄せず、未適用操作と競合理由をOfflineOperationおよびItemHistoryへ記録し、Sho-Roomから確認できるようにする。

occurred_atは監査表示用に保存するが、端末時計を信頼して競合の優先順位を決めない。競合判定はサーバーが採番するItems.revisionとbase_revisionの一致で行い、端末時計のずれがあってもサーバー側の更新を優先する。サーバー受信時刻はItemHistory.created_atおよびScanLog.created_atへ別途保存する。

返却時のstatusおよびcurrent_location_idは、D-02〜D-05で確定したパターンに従って送信する。パターンAでは2日目夜のスキャン・記録を送信せず、パターンBでは講義棟前到着時と教室収容後の全数スキャンを送信する。

GET /items/:id/history​

  • 概要: 特定備品の変更履歴とスキャン履歴を時系列で取得する

  • 権限: EQUIPMENT_VIEW

  • パスパラメータ: id (UUID)

  • クエリパラメータ:

    • start (integer, optional): 開始位置

    • end (integer, optional): 終了位置

  • レスポンス例:

{
"history": [
{
"id": 1523,
"item_id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"actor_id": "123456789012345678",
"field_name": "status",
"before_value": "LENT",
"after_value": "RETURNED",
"condition_type": "NORMAL",
"action_type": "RETURN",
"source": "FZ-N1",
"device_id": "FZ-N1-001",
"reason": null,
"occurred_at": "2024-01-26T18:00:00+09:00",
"created_at": "2024-01-26T18:00:00"
},
{
"id": 1450,
"item_id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"actor_id": "123456789012345678",
"field_name": "status",
"before_value": "ALLOCATED",
"after_value": "LENDING_SCANNED",
"condition_type": "NORMAL",
"action_type": "LENDING_SCAN",
"source": "lending-session",
"device_id": "FZ-N1-001",
"reason": null,
"occurred_at": "2026-10-09T09:01:00+09:00",
"created_at": "2026-10-09T09:01:02+09:00"
}
],
"scans": [
{
"id": "018d3f7a-scan-0000-0000-000000000001",
"item_id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"scanning_organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"allocated_project_id": "018d3f7a-project-0000-0000-000000000001",
"lending_session_id": "018d3f7a-session-0000-0000-000000000001",
"actor_id": "123456789012345678",
"scanned_by_role": "ORG_REPRESENTATIVE",
"match_result": "MATCHED",
"source": "lending-session",
"device_id": "FZ-N1-001",
"occurred_at": "2026-10-09T09:01:00+09:00",
"created_at": "2026-10-09T09:01:02+09:00"
}
],
"meta": {
"total_count": 15,
"start": 0,
"end": 50
}
}

historyには備品の変更履歴、scansには備品QRのスキャン履歴を返す。両方を発生日時順で画面に表示し、occurred_at(端末または現場で発生した日時)とcreated_at(サーバーが記録した日時)を区別する。

5.7.1 備品割当API(F-020〜F-022)​

申請システムから必要数を取り込み、講義棟出展分を除外したうえで、同一教室を優先する割当案を作成する。D-06で決定したとおり、申請システムとの連携はSho-Roomから操作し、Service Bindingsで処理する。

POST /allocations/requests/import​

  • 概要: 申請システムから企画別・備品種別別の必要数を取得する

  • 権限: EQUIPMENT_EDIT

  • 保存項目: 企画ID、団体、企画名、備品種別、必要数、取込日時

  • 取込ポリシー: 対象年度の全件スナップショットであることを明示した取込だけを原子的に反映し、project_idとitem_type_idで現在値をupsertする。各取込はimport_batch_idを発行し、変更前後の必要数、取込日時、取込結果をEquipmentRequestImportLogへ保存する。部分取得、通信失敗、対象年度不一致では既存の申請数と確定済み割当を変更しない

  • 申請数減少時: CONFIRMEDまたはLENTの割当は自動取消せず、過剰割当として警告する。新しい必要数を超えるPROPOSEDだけはREQUEST_QUANTITY_DECREASED理由でCANCELLEDに変更でき、確定済み割当の取消はEQUIPMENT_EDIT保持者が理由を入力して個別に行う

  • 企画消滅時: 全件スナップショットから企画の行が消えた場合は必要数0の取込履歴を残し、申請行・既存の確定済み割当を物理削除しない。無効企画は新しい割当案の対象外とし、既存の確定済み割当はEQUIPMENT_EDIT保持者による確認対象にする

POST /allocations/preview​

  • 概要: 除外条件と同一教室優先条件に基づく割当案を作成する

  • 権限: EQUIPMENT_EDIT

  • 処理:

    1. 講義棟出展で使用する備品または必要数を割当候補から除外する

    2. 残りの備品を必要数、備品種別、元の場所、持出先、団体、企画に基づいて候補化する

    3. 一つの企画について必要数を満たす範囲で同一教室の備品を優先する

    4. 集約できない場合は複数教室への分散と不足数を結果へ含める

  • レスポンスに含める情報: 企画、団体、備品種別、必要数、割当数、不足数、除外数、元の場所、持出先、割当候補

POST /allocations/confirm​

  • 概要: EQUIPMENT_EDIT保持者が割当案を確認して確定する

  • 権限: EQUIPMENT_EDIT

  • 処理: 確定した割当をAllocationsへ保存し、対象備品の状態と企画・持出先を更新する。Allocationsの有効割当は備品ごとに1件までとし、過年度のCANCELLED割当は制約対象外とする。確定操作と各備品の変更をItemHistoryへ記録する

PATCH /allocations/:id​

  • 概要: 確定前または確定後の個別割当をEQUIPMENT_EDIT保持者が修正する

  • 権限: EQUIPMENT_EDIT

  • 処理: 修正理由、変更前後の企画・持出先・備品を監査履歴へ記録する

5.7.2 企画管理API(F-037)​

GET /projects​

  • 概要: 参加団体に紐付く企画を年度、団体、状態で取得する

  • 権限: EQUIPMENT_VIEW

  • 備考: 企画は参加団体と別の管理単位であり、同一団体の複数企画を区別する

POST /projects​

  • 概要: 参加団体に紐付く企画を新規登録する

  • 権限: EQUIPMENT_MANAGE_TYPE

  • リクエストボディ:

{
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"name": "展示企画 A",
"external_id": "project-001",
"festival_year": 2026
}
  • バリデーション: organization_idは有効な団体、nameは必須かつ空白のみ不可、festival_yearは対象年度、external_idは指定時に年度内で一意であることを確認する

  • 処理: idにはUUID v7を自動生成し、is_activeを1で登録する

  • レスポンス: 登録された企画情報(status: 201)

PUT /projects/:id​

  • 概要: 企画情報を更新する

  • 権限: EQUIPMENT_MANAGE_TYPE

  • パスパラメータ: id (UUID)

  • リクエストボディ: organization_id、name、external_id、festival_year。各項目のバリデーションはPOST /projectsと同様とする

  • 処理: is_activeを変更せずに指定項目を更新し、既存の割当と履歴との紐付きを維持する

  • レスポンス: 更新後の企画情報

DELETE /projects/:id​

  • 概要: 企画の無効化(論理削除)

  • 権限: EQUIPMENT_MANAGE_TYPE

  • パスパラメータ: id (UUID)

  • 処理: 参照の有無にかかわらず物理削除は行わず、is_activeを0に設定して無効化する。割当と履歴から参照されている場合も削除処理は成立させ、204 No Contentを返す

5.7.3 団体貸出セッションAPI(F-023〜F-025、F-040)​

POST /lending-sessions​

  • 概要: 学祭委員がオンラインのFZ-N1で団体QRコードを読み取り、対象団体の貸出セッションを開始する。通常の団体貸出は本エンドポイントを起点とし、POST /items/organization-actionで代替しない

  • 権限: EQUIPMENT_VIEW(JWT認証済みのFZ-N1)。EQUIPMENT_BULK_OPERATIONは要求しない

  • リクエストボディ:

{
"organization_qr_payload": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"device_id": "FZ-N1-001"
}
  • 処理:

    1. 団体QRコードの平文ペイロードをOrganizations.idとして解釈し、UUID形式と有効なOrganizationsへの紐付きを確認する。署名検証や有効期限検証は行わない

    2. 同じdevice_idまたは同じ団体に未終了セッションがある場合は開始せず、409 Conflict(code: ACTIVE_LENDING_SESSION_EXISTS)で既存セッションを返す

    3. FZ-N1のログイン済み学祭委員をstarted_byとして、LendingSessionに団体ID、端末識別子、開始日時、タイムアウト日時を保存する

    4. 団体に属する当年度の企画の確定済み割当リスト(備品ID、企画ID、持出先ID)を取得し、レスポンスとしてFZ-N1へ事前ダウンロードさせる。FZ-N1はこのリストをセッション単位でローカル保存する

    5. セッションのタイムアウト日時は設定値(初期値120分)で算出する。期限を超えた未終了セッションは新しいスキャンを受け付けず、SESSION_TIMEOUTとして自動終了してから再開を許可する

  • レスポンス例:

{
"session_id": "018d3f7a-session-0000-0000-000000000001",
"organization_id": "018d3f7a-8f7h-ae5d-cg4i-6hb9f7e5i4d",
"started_at": "2026-10-09T09:00:00+09:00",
"expires_at": "2026-10-09T11:00:00+09:00",
"allocations": [
{
"item_id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"project_id": "018d3f7a-project-0000-0000-000000000001",
"destination_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b"
}
]
}
  • 備考: 団体QRコードの発行・生成・配布、Discordチャンネルへの配布は別botの担当であり、本エンドポイントでは行わない。セッション開始はオンライン必須であり、開始後の備品スキャンは事前ダウンロード済みリストを使ってオフラインでも継続できる

POST /lending-sessions/:id/items/:item_id/scan​

  • 概要: 担当者がFZ-N1でスキャンした備品を、開始済みセッションの団体への割当と照合する

  • 権限: EQUIPMENT_VIEWを持ち、有効な貸出セッションを開始した学祭委員のJWT。実際に端末を操作する担当者は別途ログインせず、リクエストのscanned_by_roleで区別する

  • リクエストボディ:

{
"device_id": "FZ-N1-001",
"occurred_at": "2026-10-09T09:01:00+09:00",
"scanned_by_role": "ORG_REPRESENTATIVE"
}
  • 処理:

    1. 貸出セッションから対象団体を取得し、備品がその団体に属する企画の確定済み割当であるかを即時判定する

    2. 一致した場合はstatusをLENDING_SCANNEDへ遷移させ、貸出時スキャン済みとして記録する。セッション終了時に一致したスキャン済み備品だけをLENTへ確定する

    3. 指定外の場合はFZ-N1に警告を表示し、持ち出し完了にはしない

    4. 一致・不一致にかかわらず、団体、備品、貸出セッション、割当先企画、発生日時、認証上の操作元、実際のスキャン担当、端末識別子、判定結果をScanLogへ記録する。通常の貸出時のsourceはlending-sessionとする

    5. オフライン時はFZ-N1がセッション開始時に保存した割当リストで同じ照合を行い、結果と操作をOfflineOperationへ保存する。オンライン復帰後、POST /items/syncで送信し、サーバーが当時の割当と状態遷移を再検証する

  • 重複スキャン: 前回のスキャン日時と結果をFZ-N1に表示する

  • 持出後の二重確認スキャン: 各団体の搬出完了後、学祭委員(備品PJ)がFZ-N1を持って各テント等を巡回する。学祭委員またはその場に居合わせた参加団体の担当者が対象備品を一つずつスキャンし、POST /items/syncへsource=placement-confirmationを付けて配置完了を確認・確定する。実際のスキャン担当はscanned_by_roleで記録し、参加団体担当者に別の認証は要求しない。貸出セッションは再開しない

POST /lending-sessions/:id/end​

  • 概要: 担当者からFZ-N1が返却されたとき、または学祭委員が明示的に終了操作を行ったときに貸出セッションを終了する

  • 権限: セッションを開始したEQUIPMENT_VIEW保持者のJWT。開始者以外が強制終了する場合はSYSTEM_MANAGEを必須とする

  • リクエストボディ:

{
"end_reason": "DEVICE_RETURNED"
}
  • 処理: LendingSessionに終了日時、終了理由、終了操作元を保存する。end_reasonはDEVICE_RETURNED(端末返却)、STAFF_TERMINATED(学祭委員による明示的終了)またはSESSION_TIMEOUT(タイムアウト)とする。終了時にLENDING_SCANNEDのうち一致したスキャン済み備品をLENTへ遷移し、未スキャンの割当済み備品はALLOCATEDのまま残す。各遷移をItemHistoryへ記録する

5.7.4 ラベル・監査API(F-017〜F-019、F-030〜F-031)​

POST /items/:id/label/print​

  • 概要: 備品ラベルを発行し、Epson TM-L90へ印刷する

  • 権限: EQUIPMENT_EDIT

  • 印刷内容: QR、元の場所、持出先、使用団体、使用企画。行き先は人が読める文字列で印刷し、記号だけの表現は使用しない

  • 処理: 発行番号、発行日時、発行者、印刷結果、再発行理由をLabelPrintLogへ記録する。同一備品の再発行では理由を必須とする

POST /labels/attachment-check​

  • 概要: 教室・場所単位で貼付済みラベルをスキャンし、重複、不足、別教室への混入を確認する

  • 権限: EQUIPMENT_EDIT

  • 処理: 確認者、日時、場所、備品、一致結果をLabelAttachmentCheckLogへ記録する

GET /audit-logs​

  • 概要: 備品変更履歴とスキャン履歴を検索・閲覧する

  • 権限: EQUIPMENT_VIEW

  • 検索条件: 備品、期間、操作種別、実行者、団体、企画、操作元、端末識別子

  • 制約: 監査ログの通常更新・削除は許可しない

5.8 ダッシュボードAPI(Dashboard)​

GET /dashboard/summary​

  • 概要: ダッシュボード用の集計データ取得

  • 権限: EQUIPMENT_VIEW

  • レスポンス例:

{
"metrics": {
"total_items": 1523,
"lent_items": 432,
"stock_items": 1089,
"needs_attention": 2
},
"recent_logs": [
{
"id": 1523,
"item_display_id": "DESK-001",
"item_type_name": "長机",
"actor_id": "123456789012345678",
"action_type": "RETURN",
"created_at": "2024-01-26T18:00:00"
}
]
}

GET /dashboard/location-stats​

  • 概要: 場所ごとの備品状況を集計

  • 権限: EQUIPMENT_VIEW

  • レスポンス例:

{
"storage_view": [
{
"location_id": "018d3f7a-5c4e-7b2a-9d1f-3e8a6c4b2f1a",
"location_name": "M1",
"original_count": 500,
"current_count": 320
}
],
"venue_view": [
{
"location_id": "018d3f7a-6d5f-8c3b-ae2g-4f9b7d5c3g2b",
"location_name": "講義棟",
"current_count": 180
}
]
}

5.9 システム管理API(System)​

Google SpreadsheetとのExport / ImportはScope Outであり、/system/sync/exportおよび/system/sync/importは本システムの提供APIとしない。備品データの正本はEquipment DBである。

POST /system/reset​

  • 概要: 年度更新用全リセット

  • 権限: EQUIPMENT_RESET

  • 処理ロジック:

    1. 全ItemsのstatusをSTOCKにリセット

    2. current_location_idをoriginal_location_idに戻す

    3. destination_id、organization_id、project_idを初期化し、過年度のPROPOSED・CONFIRMED割当は削除せずCANCELLEDへ変更する。cancelled_at、cancelled_by、cancel_reasonに年度更新の情報を記録する

    4. 進行中のLendingSessionに終了日時とYEAR_RESETの終了理由を記録し、終了扱いにする。未終了セッションの一時割当もこの処理で確定させず、監査履歴へ終了理由を記録する

    5. ItemHistoryに”RESET”ログを記録し、年度更新操作を監査ログへ記録

    6. 過去の変更履歴、スキャン履歴、通知履歴、ラベル履歴は削除せずアーカイブ状態で保持

  • レスポンス例:

{
"reset_count": 1523,
"timestamp": 1706745600
}

GET /system/export/all​

  • 概要: 学祭準備期間用全データエクスポート(大学環境のcronサーバー用)

  • 権限: 専用API Key認証(JWT認証不使用)

  • 認証方式:

    • ヘッダー: X-API-Key: <API_KEY>

    • 環境変数CRON_API_KEYと照合

  • レスポンス例:

{
"items": [
{
"id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"display_id": "DESK-001",
"item_type_name": "長机",
"status": "LENT",
"condition": "NORMAL",
"original_location_name": "M1",
"current_location_name": "講義棟",
"destination_name": "講義棟",
"organization_name": "展示ブース A",
"project_name": "展示企画 A",
"note": "脚部に傷あり",
"updated_at": "2026-01-26T09:00:00"
}
],
"total_count": 1523,
"exported_at": "2026-01-31T15:00:00"
}
  • 備考:

    • 全備品データを一括で返却(ページネーションなし)

    • 呼出頻度、保存先、保存期間は運用設計で定める

    • このエクスポートはCloudflareのバックアップ・リストアの代替ではない

5.10 特殊操作API(Special Operations)​

特殊操作APIは、備品状態の強制変更と権限確認・監査を提供する。POST /special/force-status-changeにはEQUIPMENT_EDIT、その他の3エンドポイント(GET /special/staff-list、POST /special/staff-by-nfc、GET /special/authority-changes)にはSYSTEM_MANAGEが必要であり、各エンドポイントは対応する権限ビットを持つユーザーのみ実行できる。

NFC識別情報の登録・再登録​

学祭委員名簿(Staffs)の登録・管理およびFeliCa IDmの登録・再登録は、既存認証基盤または名簿供給元の責務であり、本システムの対象外とする。備品管理APIは、FZ-N1から受け取った学籍番号とIDmをStaffs.StudentIdおよびStaffs.NfcIdmへ照合し、認証基盤から得たDiscord ID・権限を利用する。IDm未登録時に学籍番号だけでログインを許可しない。

POST /special/force-status-change​

  • 概要: 備品状態の強制変更(緊急時の異常状態修正)

  • 権限: EQUIPMENT_EDIT

  • リクエストボディ:

{
"item_id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"status": "STOCK",
"condition": "NORMAL",
"organization_id": null,
"reason": "誤操作により状態が異常になったため強制修正"
}
  • 処理ロジック:

    1. 指定された備品の状態を強制的に変更

    2. ItemHistoryに特殊操作ログを記録(理由を含む)

  • レスポンス例:

{
"item_id": "018d3f7a-bi0k-dh8g-fj7l-9ke2i0h8l7g",
"display_id": "DESK-001",
"updated_fields": ["status", "condition", "organization_id"]
}

GET /special/staff-list​

  • 概要: 学祭委員一覧の取得

  • 権限: SYSTEM_MANAGE

  • クエリパラメータ:

    • start (integer, optional): 開始位置(デフォルト: 0)

    • end (integer, optional): 終了位置(デフォルト: 50)

    • search (string, optional): 学籍番号・氏名で検索

  • レスポンス例:

{
"staffs": [
{
"discord_id": "123456789012345678",
"student_id": "s1234567",
"staff_name": "山田 太郎",
"permissions": 0
}
],
"total": 150,
"start": 0,
"end": 50
}

POST /special/staff-by-nfc​

  • 概要: NFCスキャンによる学祭委員情報の参照(登録・更新は行わない)

  • 権限: SYSTEM_MANAGE

  • リクエストボディ:

{
"student_id": "s1234567",
"idm": "0123456789ABCDEF"
}
  • 処理ロジック:

    1. student_idとidmを受信した場合は正規化し、System DBの同一StaffsレコードについてStudentIdとNfcIdmの両方を照合する。片方だけが指定された場合も、指定された識別子に対応するレコードのNfcIdmを確認する

    2. レコードなし、またはIDmが一致しない → 404 Not Found(code: STAFF_NOT_FOUND)。照合失敗を権限不足と混同しない

    3. レコードあり → スタッフ情報を返却。IDmの登録・再登録は既存認証基盤または名簿供給元で行い、本APIでは変更しない

  • レスポンス例:

{
"discord_id": "123456789012345678",
"student_id": "s1234567",
"staff_name": "山田 太郎",
"permissions": 0
}

権限変更の扱い​

権限の正本はDiscordロールであり、備品管理APIからStaffsの権限を直接変更するエンドポイントは提供しない。本システムはDiscord上で実際にロールを変更した利用者や理由を取得できないため、ログインまたは権限再計算時に前回記録した権限ビットと現在のStaffs.Authorityが異なる場合、変更前後の権限ビットと対象をAuthorityChangeLogへactor_id=SYSTEM、reason=AUTHORITY_RECALCULATED_ON_LOGINとして記録する。Discord側の実行者・理由の監査はDiscord監査ログを参照する。

GET /special/authority-changes​

  • 概要: 権限変更監査履歴の確認

  • 権限: SYSTEM_MANAGE

  • クエリパラメータ:

    • start (integer, optional): 開始位置(デフォルト: 0)

    • end (integer, optional): 終了位置(デフォルト: 50)

  • レスポンス: 対象者、変更前後の権限ビット、actor_id(本システムの自動検出はSYSTEM)、理由、日時。権限の変更操作自体はDiscordサーバーで行う


6. エラー処理とログ​

6.1 バックエンド(Hono)​

  • エラーハンドリング: 例外を捕捉し、機密値を除いた構造化エラーとして扱う

  • ログ送信: Service Binding等を使用した外部エラーログシステムへ送信する

  • エラーレスポンス: HTTPステータスコードと利用者向けの一般化したメッセージを返却する。内部のスタックや認証情報を返さない

  • 機密情報の除外: JWT、Cookie、カード情報、学籍番号、セッションIDをログへ出力しない

// エラーハンドリング例
try {
// 処理
} catch (error) {
// 機密値を除外してService Bindingでログ送信
await env.ERROR_LOG.send({
message: sanitizeErrorMessage(error),
timestamp: Date.now(),
});

// クライアントにエラーレスポンス
return c.json(
{
status: 500,
code: 'INTERNAL_SERVER_ERROR',
message: '内部エラーが発生しました',
},
500,
);
}

6.2 フロントエンド(Web App)​

  • エラー収集: エラーを内部で収集し、認証情報を除外する

  • ログ送信: Sho-Roomのエラーログ送信機能と連携して外部ログシステムへ送信する

  • エラー表示: ユーザーにわかりやすい一般化したメッセージを表示する

6.3 FZ-N1アプリ​

  • エラー表示: ダイアログまたはトースト通知でエラーを表示する

  • ログ記録: ローカルに機密情報を含めず保存し、オンライン時に送信する

  • 音声フィードバック: エラー時はBeep音で通知する


7. ハンディアプリ仕様(FZ-N1 / Android 6.0.1)​

7.1 開発環境​

  • Android Version: 6.0.1 (API Level 23)

  • minSdk: 23

  • 言語: Java 8

  • 通信: HttpURLConnection または OkHttp 3.x(TLS 1.2対応必須)

  • ビルドコマンド: ./gradlew clean assembleDebug

  • Toughpad SDK:

    • Panasonic提供のToughpad SDK (.jar形式)

    • 物理ボタン制御用ライブラリ

    • A1-A3ボタン、サイドボタンのイベントハンドリング

7.1.1 物理ボタン仕様​

FZ-N1には以下の物理ボタンが搭載されています:

画面下部の3つのボタン:

  • A1ボタン (左): キャンセル・拒否操作

  • A2ボタン (中央): 補助操作(画面により機能が変わる)

  • A3ボタン (右): 確認・承認操作

サイドボタン:

  • 左サイドボタン: 補助操作(現時点では未使用)

  • 右サイドボタン: 補助操作(現時点では未使用)

動的な割り当て原則:

  • 物理ボタンは 必要な画面でのみ機能を割り当て、不要な画面では何も動作しない

  • 各画面で使用するボタンのみリスナーを登録する

  • 使用しないボタンはイベントを無視する

画面ごとの割り当て例:

画面A1ボタンA2ボタンA3ボタン
一括操作確認キャンセル概要/詳細切替実行
ログイン---
メインメニュー---
個別スキャン確認キャンセル-保存

Toughpad SDK使用例:

// Toughpad SDKのインポート
import jp.co.panasonic.toughpad.android.api.appbtn.AppButtonManager;
import jp.co.panasonic.toughpad.android.api.appbtn.AppButtonManager.AppButtonListener;

// ボタンイベントリスナーの設定
AppButtonManager manager = new AppButtonManager(this);

// 一括操作確認画面の場合
manager.setAppButtonListener(new AppButtonListener() {
@Override
public void onAppButtonEvent(int buttonId, int event) {
if (event == AppButtonManager.APP_BUTTON_EVENT_DOWN) {
switch (buttonId) {
case AppButtonManager.APP_BUTTON_A1:
// A1ボタン(キャンセル)の処理
onCancelButtonPressed();
break;
case AppButtonManager.APP_BUTTON_A2:
// A2ボタン(概要/詳細切替)の処理
toggleDetailView();
break;
case AppButtonManager.APP_BUTTON_A3:
// A3ボタン(確認)の処理
onConfirmButtonPressed();
break;
}
}
}
});

// 画面を離れる時はリスナーを解除
@Override
protected void onDestroy() {
super.onDestroy();
if (manager != null) {
manager.release();
}
}

画面表示イメージ(一括操作確認画面):

┌─────────────────────────┐
│ │
│ (メインコンテンツ) │
│ │
├─────────────────────────┤
│ │
│ [キャンセル] [切替] [実行] │ ← A1, A2, A3ボタンに対応
│ │
└─────────────────────────┘
  • A1ボタンを押すと「キャンセル」

  • A2ボタンを押すと「概要/詳細切替」

  • A3ボタンを押すと「実行」

  • 画面下部には物理ボタンに対応する表示を配置

7.2 認証フロー詳細​

アプリ起動時はログイン画面を表示する。

FZ-N1は学生証のFeliCaブロックデータから学籍番号とIDmを読み取り、System DBのStaffsをStudentIdとNfcIdmの組み合わせで照合する。学籍番号だけでのフォールバックは行わず、照合と権限確認が成功した時点でログインを成立させる。

A. NFCログイン(Primary Flow)​

ステップ1: NFCリーダー待機

  1. アプリ起動後、NFCリーダーを有効化しFeliCaをポーリングする

  2. 画面に「学生証をタッチしてください」と表示する

ステップ2: FeliCa検知と読取

  • System Code: 0x809E

  • PMm: 03 32 42 82 82 47 AA FF(会津大学学生証識別)

  • 読取ロジック:

  1. IDm(8byte)を取得する

  2. Service Code 0x300B Block 0を読み取る

  3. 取得した16byteデータから学籍番号を抽出し、先頭にsを付与してs1234567形式にする

  4. 対応しないカードはエラー音(Beep)を鳴らし、「無効なカードです」と表示する

ステップ3: API照合

  • POST /auth/nfcを送信

  • Body: { "student_id": "s1234567", "idm": "0123456789ABCDEF" }

  • レスポンス処理:

    • 成功(200):
1. **成功音を鳴らす**
2. **確認画面を表示**(学籍番号を表示)
3. 「OK」ボタンタップで **メインメニューへ遷移**
  • 名簿未登録または権限不足(403): 警告音 + エラーダイアログ「権限がありません」

ログイン成功後のDiscord通知は別処理として実行する。Discord APIの障害や権限不足で通知できない場合もFZ-N1のログインは成立させる。

確認画面UI:

  • タイトル: 「ログイン確認」

  • 表示項目:

    • 学籍番号: s1234567
  • ボタン: 「OK」

7.3 メニュー・ナビゲーション​

EQUIPMENT_VIEWを持つ学祭委員がログインした場合、FZ-N1メインメニューを表示する。メニュー内の各機能は、追加の権限ビットが必要な場合にそのビットを持つ利用者だけへ表示する。

  1. 団体貸出セッション(Organization Lending Session): EQUIPMENT_VIEWで利用する通常の団体貸出。団体QRを読み取り、割当照合付きの貸出セッションを開始する

  2. 個別スキャン(Single Scan): EQUIPMENT_VIEWで個別確認を行う。状態を変更する操作はEQUIPMENT_EDITを持つ場合だけ表示・実行する

  3. 一括操作(Bulk Operation / 例外処理): EQUIPMENT_BULK_OPERATION保持者だけが使用する一括貸出・返却。通常の団体貸出には使用しない

  4. 設定(Settings): ログアウト、未送信データ確認

7.3.1 貸出セッション中のキオスク制約​

LendingSession.ended_atが未設定の間、FZ-N1は貸出セッション画面をキオスクとして扱う。

  • 団体担当者へ端末を手渡している間は、備品の貸出スキャン画面以外へ遷移できない

  • 設定画面、個別スキャン、一括操作、ログアウト、通常の戻る操作を無効化する

  • セッション終了は、端末返却後に学祭委員が明示操作した場合、またはセッション終了APIが受理された場合だけ行う。団体担当者が終了操作やログアウトを行うことはできない

  • アプリが中断・再起動しても未終了セッションをローカルに保持し、貸出セッション画面へ復帰する。未送信スキャンがある場合は同期完了または明示的な競合確認まで終了を確定しない

7.3.2 ログアウト機能​

ログアウト方法:

  • メニューから「設定」を選択し、「ログアウト」ボタンをタップする

  • アプリ終了時はローカルのJWTを再利用できない状態にする

ログアウト処理:

  1. ローカルに保存されたJWTを削除する

  2. 未送信データがある場合、確認ダイアログを表示する

    • 「送信してログアウト」: 未送信データを送信後にログアウトする

    • 「データを破棄してログアウト」: 利用者が明示的に選択した場合だけ未送信データを破棄してログアウトする

    • 「キャンセル」: ログアウトを中止する

  3. ログイン画面へ遷移する

7.4 機能詳細: 前日の団体貸出(教室での学祭委員立会い)​

団体QRコードの発行・生成・配布は別botの担当であり、本システムでは行わない。本システムは、オンラインで学祭委員がFZ-N1で団体QRの平文UUIDを読み取り、対象団体の確定済み割当リストを端末へ事前ダウンロードして貸出セッションを開始する。その後の備品スキャンはオンライン・オフラインを問わず端末上で割当照合し、オンライン復帰後にサーバーへ同期する。各団体の搬出完了後は、学祭委員(備品PJ)がFZ-N1を持って各テント等を巡回し、学祭委員またはその場に居合わせた参加団体の担当者が二重確認スキャンを行って配置完了を確認・確定する。

貸出フロー:

  1. 教室に監視の学祭委員が常駐する

  2. 参加団体の企画担当者が教室に来る

  3. 学祭委員がオンラインのFZ-N1で団体のQRコードを読み取る。QRの平文ペイロードはOrganizations.idのUUIDであり、団体別QRコードは別botが出展者サーバーの各団体チャンネルへ配布するため、本システムは発行・生成・配布を扱わない

  4. サーバーが貸出セッションを作成し、対象団体の当年度の確定済み割当リストをFZ-N1へ事前ダウンロードする。通信が切れる前に、セッションID、団体ID、企画ID、備品ID、持出先IDを端末へ保存する

  5. 学祭委員がログイン済みのFZ-N1を団体の担当者へ手渡す。セッション中はキオスク制約により、担当者は貸出スキャン画面以外へ遷移できない

  6. 担当者自身がFZ-N1で対応する備品のQRコードを一つずつスキャンする。オンライン時はサーバー、オフライン時は端末に保存した割当リストで照合し、一致すればLENDING_SCANNEDとして記録する。指定外の場合は警告を表示して持ち出し完了にはしない

  7. 担当者が対応する備品をテント等の設置場所へ搬出する

  8. 担当者がFZ-N1を学祭委員へ返却する。端末返却を受けて学祭委員がセッションを終了すると、一致したLENDING_SCANNEDだけがLENTへ確定し、未スキャンの割当済み備品はALLOCATEDのまま残る。端末がオフラインの場合は、未送信操作を同期してから終了を確定する

  9. 各団体が対象備品をテント等の設置場所へ搬出完了した後、学祭委員(備品PJ)がFZ-N1を持って各テント等を巡回する

  10. 学祭委員またはその場に居合わせた参加団体の担当者が、FZ-N1で対象備品を一つずつ二重確認スキャンする。source=placement-confirmationとscanned_by_roleを付けてPOST /items/syncへ送信し、配置完了を確認・確定する。実際のスキャン担当は運用で決め、参加団体担当者に別の認証は要求しない

セッションと記録:

  • 貸出セッションには対象団体ID、開始日時、終了日時、開始した学祭委員ID、終了操作元、FZ-N1の端末識別子を記録する

  • 貸出時スキャンの照合対象は、セッション開始時に端末へ取得した対象団体の当年度の確定済み割当とする。オンライン復帰時にはサーバーが再検証し、企画担当者のログインや、団体担当者向けのCookieセッションは使用しない

  • 一致・不一致にかかわらず、スキャンした団体、備品、日時、貸出セッション、割当先企画、認証上の操作元、実際のスキャン担当、端末識別子、判定結果を記録する

  • 持出後の二重確認スキャンは貸出時スキャンとは別の配置確認として記録し、配置完了の確認・確定結果と対象備品を追跡できるようにする

  • 持出後の二重確認スキャンは貸出セッション終了後の個別スキャンとして扱い、オンライン時もオフライン復帰時もsource=placement-confirmationを付けたPOST /items/syncで同期する。貸出セッションを再開しない

返却フロー(D-02〜D-05、スパイス交渉結果待ち):

テント等のレンタル業者(スパイス)の回収タイミング交渉結果により、次の2パターンのいずれかに最終確定する。現時点ではD-02〜D-05の最終決定を保留しており、参加団体による返却時のセルフスキャンは行わない。

項目パターンA:スパイスが回収を遅らせてくれる場合パターンB:スパイスが例年通り回収する場合
D-02(返却成立条件)片付けの日(2026/10/12)に講義棟前へ返却された時点を返却成立とする昨年通り、講義棟前に備品が返却された時点を返却成立とする。ただし、この運用が学生課等から承認されるかは未確定
D-03(2日目夜の記録)記録しない。2日目夜はスキャン・記録を行わない講義棟前到着時に全数スキャンを行う。到着時点で備品への責任が備品課に移るため
D-04(中間集積場所を現在地として記録するか)記録しない。講義棟前を中間集積場所としてcurrent_location_idに記録しない混乱防止のため、講義棟前を現在地として記録する
D-05(足拭き・教室収容)備品課・学祭委員が講義棟前へ到着次第、逐次スキャン・足拭き・教室収容を行う。可能であれば、備品を持ってきた各団体自身に足拭き・教室搬入まで行ってもらう運用を志向する備品課・学祭委員がテント等の片付け後に足拭きを行って教室へ収容し、その後に全数スキャンを行う

7.5 機能詳細: 個別スキャン​

動作フロー:

  1. バーコードをスキャン(備品のUUID v7が格納されたQRコード)

  2. API検索(GET /items/:id)

  3. 結果表示

表示内容:

  • Display ID(DESK-001等)を画面中央に大きく表示

  • 現在の貸出状態(割当前/割当済み/貸出時スキャン済み/貸出中/返却済み)と備品状態(正常/要修理/破損等)を表示

  • 備品種別、元の場所、現在地・持出先、使用団体、使用企画を表示

操作:

  • その場でCondition(正常/破損/…)やStatus(割当/貸出…)を事前定義された選択肢から変更する

  • 自由記述の備考が必要な場合はSho-Roomから入力する

  • 変更後、操作IDを付けてPOST /items/syncで送信する

7.6 オフライン対応​

基本動作:

  • ネットワーク接続が切れた場合、操作結果を一意な操作ID付きでローカルに保存する

  • 団体貸出セッションの開始(団体QR読み取り)はオンライン必須とする。開始時に対象団体の当年度の確定済み割当リストを端末へ事前ダウンロードし、セッションID、団体ID、企画ID、備品ID、持出先ID、取得日時をローカルに保存する

  • 貸出セッション中の備品スキャンは、オフラインでも端末に保存した割当リストで照合する。一致時はローカルでLENDING_SCANNEDとして扱い、指定外の場合は警告を表示して状態を変更しない。不一致を含むスキャン結果も監査送信用の操作として保存する

  • オフラインスキャン時に、操作日時occurred_at、端末識別子device_id、認証上の操作元actor_id、実際のスキャン担当scanned_by_role、source、貸出セッションIDを保存する

  • オンライン復帰時にPOST /items/syncで未送信操作を送信する

  • 未送信データがある場合、設定画面で確認・再送信できる

競合処理:

  • 同期時に各備品のサーバー側revisionを取得し、オフライン開始時に保存したbase_revisionと比較する

  • base_revisionが一致する場合だけ状態更新を適用し、受理時にサーバー側のrevisionを1つ進める。occurred_atは監査表示用に保存し、端末時計を信頼して競合の勝敗を決めない

  • サーバー側の更新が先行している場合は、操作を単純に破棄せずOfflineOperation.conflict_status=CONFLICTとして保存し、ScanLogまたはItemHistoryに未適用理由を記録する

  • source=placement-confirmationの二重確認スキャンも同じ同期経路で送信する。サーバーはsourceとscanned_by_roleを使って貸出時スキャンと区別し、貸出セッションを再開せずに配置確認として記録する

  • 未適用操作、競合理由、対象備品を同期結果画面とSho-Roomで確認できるようにする

7.7 音声フィードバック​

  • 成功音: 操作成功時(貸出/返却完了、ログイン成功)

  • エラー音: エラー発生時(無効なカード、権限なし、通信エラー)

  • 警告音: 注意が必要な場合(破損備品の検出など)


8. Webアプリ仕様(Sho-Roomモジュール)​

8.1 開発環境​

基本構成​

  • フレームワーク: React 18+ (TypeScript)

  • スタイリング: Tailwind CSS

  • ビルドツール: Vite

  • デプロイ: Cloudflare Workers + Static Assets

  • BFF: Cloudflare Workers(Hono, 同一Worker内でAPIプロキシを実装)

  • 対象デバイス: PCおよびモバイル端末(レスポンシブデザイン)

プロジェクト構造​

sho-room/
├── worker/ # Cloudflare Workers(BFF)
│ ├── index.ts # Honoエントリポイント / ルーティング
│ ├── proxies.ts # APIプロキシ
│ └── utils.ts # JWT抽出等の共通ユーティリティ
├── src/
│ ├── components/ # Reactコンポーネント
│ ├── hooks/ # カスタムフック
│ ├── utils/ # ユーティリティ関数
│ ├── services/ # API通信ロジック
│ ├── types/ # TypeScript型定義
│ ├── App.tsx
│ └── main.tsx
├── tests/ # テストファイル
│ ├── unit/ # 単体テスト
│ └── integration/ # 統合テスト
├── public/
├── package.json
├── vite.config.ts
├── vitest.config.ts
└── tsconfig.json

BFF実装(Sho-Room Worker、Cloudflare Workers)​

worker/utils.ts(CookieからのJWT抽出):

// Cloudflare Workers - BFFプロキシ共通ユーティリティ

export interface WorkerEnv {
ASSETS: Fetcher;
GATEWAY: Fetcher; // Service Binding経由でapi.soshosai.comへ
}

/** Cookie から staff JWT を取得。無ければ null */
export function extractJwt(request: Request): string | null {
const cookies = request.headers.get('Cookie') ?? '';
return cookies.match(/soshosai_staff_jwt=([^;]+)/)?.[1] ?? null;
}

worker/index.ts(ルーティング):

// wrangler.jsonc の assets.run_worker_first: ["/api/*"] により
// /api/* のみ Worker が先に実行され、それ以外は静的アセット配信が先に走る。

import { extractJwt, type WorkerEnv } from './utils';

export default {
async fetch(request, env: WorkerEnv): Promise<Response> {
const { pathname } = new URL(request.url);

if (pathname.startsWith('/api/')) {
const jwt = extractJwt(request);
if (!jwt) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
// CookieのJWTをAuthorizationヘッダーに変換してService Binding経由で転送
const headers = new Headers(request.headers);
headers.set('Authorization', `Bearer ${jwt}`);
return env.GATEWAY.fetch(request.url, { ...request, headers });
}

// 静的アセット / SPA フォールバック
return env.ASSETS.fetch(request);
},
};

認証フロー​

  1. ログイン: login-apiがOAuth/Discordコールバックを処理し、discord-auth-workerがJWTを発行する。login-apiは発行されたJWTをsoshosai_staff_jwt Cookieへ設定する。認証処理やCookie発行の詳細は本システムの対象外とする

  2. API通信: React側は相対パスで呼び出し、Sho-Room Workerの既存BFFがsoshosai_staff_jwt CookieからJWTを取得してAuthorization: Bearer ...ヘッダーを付与し、備品管理APIへ中継する

  3. API認証: 備品管理APIは受信したJWTを既存の共通認証基盤で検証し、権限ビットに基づいて要求を認可する

  4. ログアウト: login-api等の共通認証基盤が担当する。本システムではログアウト処理、Cookie削除、セッション失効の詳細を定義しない

React側の実装例:

// src/services/auth.ts
import { SoshosaiAuth } from '@soshosai/auth-client';
import { LOGIN_API_URL } from '../config';

export const auth = new SoshosaiAuth({ baseUrl: LOGIN_API_URL });
// src/services/api.ts
import { auth } from './auth';

export async function fetchItems() {
// 相対パスで呼ぶだけ(Cookieは自動送信される)
const response = await fetch('/api/items');

if (!response.ok) {
if (response.status === 401) {
// 未認証または期限切れ。遷移先URLは手で組み立てず共通SDKに任せる
// (エンドポイント名の選択と redirect のエンコードはSDKが担う)
auth.login('discord');
return;
}
throw new Error('API request failed');
}

return response.json();
}

重要: ログインURLを window.location.href で手組みしないこと。エンドポイント名(/discord-login)の誤りや redirect のエンコード漏れは、login-api 側の validateRedirectUrlForEnv を通らずフォールバックされる原因になる。組み立ては共通SDK @soshosai/auth-client の login(provider) に一任する。

なお Sho-Room 内では、この SDK を AuthProvider がラップしている。コンポーネントからは useAuthContext().login() を呼べばよい。

開発サーバー​

# Vite開発サーバー(BFFはwrangler devで別途起動)
npm run dev

# または
wrangler dev

デプロイ​

# Cloudflare Workers(Static Assets)にデプロイ
npm run build
wrangler deploy --env <environment>

8.2 テスト仕様​

テストフレームワーク​

  • テストランナー: Vitest

  • テストライブラリ: @testing-library/react

  • モック: vi (Vitestビルトイン)

テスト戦略​

単体テスト (Unit Tests):

  • 対象: ユーティリティ関数、カスタムフック、純粋な関数

  • 配置: tests/unit/

  • 命名規則: .test.ts または .test.tsx

  • カバレッジ目標: 80%以上

統合テスト (Integration Tests):

  • 対象: コンポーネント + API通信、複数コンポーネントの連携

  • 配置: tests/integration/

  • モック: APIレスポンスをモック

ファイル分割規則​

厳密なファイル分割:

// ❌ 悪い例: 1ファイルに複数の責務
// utils.ts
export function formatDate(date: Date) { ... }
export function validateEmail(email: string) { ... }
export function calculateTotal(items: Item[]) { ... }

// ✅ 良い例: 1ファイル1責務
// date.ts
export function formatDate(date: Date) { ... }

// validation.ts
export function validateEmail(email: string) { ... }

// calculation.ts
export function calculateTotal(items: Item[]) { ... }

テストファイルの対応:

src/utils/date.ts → tests/unit/utils/date.test.ts
src/services/api.ts → tests/unit/services/api.test.ts
src/components/Item.tsx → tests/integration/Item.test.tsx

vitest.config.ts​

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.test.{ts,tsx}',
'vite.config.ts',
'vitest.config.ts',
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});

テスト例​

単体テスト例(ユーティリティ関数):

// tests/unit/utils/date.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate } from '@/utils/date';

describe('formatDate', () => {
it('should format date to YYYY-MM-DD', () => {
const date = new Date('2026-01-31T15:00:00');
expect(formatDate(date)).toBe('2026-01-31');
});

it('should handle invalid date', () => {
expect(() => formatDate(new Date('invalid'))).toThrow();
});
});

統合テスト例(コンポーネント + API):

// tests/integration/ItemList.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { ItemList } from '@/components/ItemList';
import * as api from '@/services/api';

vi.mock('@/services/api');

describe('ItemList', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should display items from API', async () => {
vi.spyOn(api, 'fetchItems').mockResolvedValue([
{ id: '1', display_id: 'DESK-001', item_type_name: '長机' },
]);

render(<ItemList />);

await waitFor(() => {
expect(screen.getByText('DESK-001')).toBeInTheDocument();
expect(screen.getByText('長机')).toBeInTheDocument();
});
});

it('should show error on API failure', async () => {
vi.spyOn(api, 'fetchItems').mockRejectedValue(new Error('API Error'));

render(<ItemList />);

await waitFor(() => {
expect(
screen.getByText(/エラーが発生しました/),
).toBeInTheDocument();
});
});
});

テスト実行コマンド​

# すべてのテストを実行
npm run test

# ウォッチモード
npm run test:watch

# カバレッジ計測
npm run test:coverage

# 特定のファイルのみ
npm run test -- date.test.ts

8.3 ダッシュボード(Dashboard)​

EQUIPMENT_VIEW保持者がログイン後に見る備品管理の初期画面。

表示要素:

  • ステータスカード: 「総備品数」「貸出中」「在庫」「要対応(NEEDS_REPAIR、UNDER_REPAIR、BROKEN、LOST)」を表示する

  • 在庫分布チャート: 場所ごとの在庫数を棒グラフで可視化する

  • 場所別ステータス確認:

    • GET /dashboard/location-statsの結果を表示する

    • 元の場所視点(在庫チェック用)と持出先視点(貸出チェック用)のテーブルを表示する

  • 最近の操作: ItemHistoryから最新5件を表示する

8.4 備品管理(Inventory)​

一覧表示:

  • カラム: Display ID, 種別, 貸出状態, コンディション(日本語), 元の場所, 現在地, 持出先, 使用団体, 使用企画

  • サーバーサイドページネーションを実装する(1ページ50件など。表示件数は変更可能: 10/20/50/100/200)

  • モバイル表示時: カード形式レイアウト等で見やすく調整する

検索パネル:

  • フリーワード: display_id、備品種別名、団体名、企画名(Debounce対応)

  • フィルタ: 種別、貸出状態、場所、持出先、コンディション、団体、企画(複数選択可)

編集モーダル:

  • 備品情報の編集(display_idの変更、備品種別、使用団体、使用企画、元の場所、現在地、持出先、コンディション変更)

  • ラベル印刷ボタン(TM-L90への送信)。再発行時は理由の入力を必須とする

  • 全項目の変更前後、実行者、日時、操作元、端末識別子を監査履歴へ記録する

カメラQR読取:

  • ブラウザAPIを使用し、備品QRをスキャンして備品詳細または編集モーダルを開く

バリデーション:

  • display_id: 必須、1-50文字、重複不可、空白のみ不可

  • note: 任意、最大500文字

8.5 場所管理(Locations)​

一覧表示:

  • 全場所をテーブル形式で表示する

  • 備品数(総数、貸出中、在庫)を表示する

CRUD操作:

  • 場所の新規作成、編集、論理削除(is_active=0)を行う

  • 参照の有無にかかわらず物理削除は行わず、参照中でもis_active=0に更新して無効化する

8.6 団体管理(Organizations)​

一覧表示:

  • 全団体をテーブル形式で表示する

  • 割り当てられている備品数と所属企画数を表示する

CRUD操作:

  • 団体の新規作成、編集、論理削除を行う

  • 参照の有無にかかわらず物理削除は行わず、参照中でもis_active=0に更新して無効化する

8.7 特殊操作(Special Operations)​

アクセス: メニュー1(備品状態の強制変更)はEQUIPMENT_EDIT、メニュー2(権限確認)はSYSTEM_MANAGE権限を持つユーザーのみ利用できる

機能メニュー:

  1. 備品状態の強制変更

    • 備品検索(Display IDまたはQRスキャン)

    • 現在の状態表示

    • 変更後の状態選択(貸出状態、コンディション、団体、企画、場所)

    • 理由入力欄(必須、最大500文字)

    • 実行前に確認ダイアログを表示

    • API: POST /special/force-status-change

  2. 権限確認(権限の変更はDiscordロール管理で行う)

    • 一覧から学祭委員を選択し、現在のpermissionsビット値を表示する(API: GET /special/staff-list)

    • NFCスキャンで学祭委員を検索し、該当する情報を表示する(API: POST /special/staff-by-nfc)

    • Discordロールから計算された変更前後の権限、対象、actor_id=SYSTEM、reason=AUTHORITY_RECALCULATED_ON_LOGINをAuthorityChangeLogへ記録する。Discord上の実際の変更者はDiscord監査ログで確認する

注意事項:

  • 全ての特殊操作は操作ログに詳細記録する

  • 実行前に必ず確認ダイアログを表示する

  • 強制変更の理由入力を必須とする

  • 権限はDiscordロールから自動計算される。本システムからStaffsの権限を直接変更しない

編集・削除:

  • モーダルで実施する

  • 場所、団体、企画、備品種別の削除はすべて論理削除(is_active=0)とし、参照中でも削除処理を成立させる。物理削除は行わない

項目:

  • 場所名(name)の編集に対応する

8.8 備品種別管理(ItemTypes)​

場所管理と同様の構成。

8.9 システム管理(System)​

各パネルはDiscord上の役職名ではなく、操作に対応する権限ビットで表示・実行を制御する。備品割当はEQUIPMENT_EDIT、場所・団体・企画・備品種別の管理は各EQUIPMENT_MANAGE_*、監査・通知確認はSYSTEM_MANAGE、年度更新はEQUIPMENT_RESETを必須とする。

備品割当パネル:

  • 申請システムから企画ごと・備品種別ごとの必要数を取り込む

  • 講義棟出展用備品の除外条件を設定・確認する

  • 同一教室優先の割当案を作成し、不足数・除外数・分散状況を表示する

  • EQUIPMENT_EDIT保持者が手動修正して確定する。確定と修正は変更履歴へ記録する

監査・通知パネル:

  • Discordログイン通知の作成、送信、削除失敗を確認する

  • 貸出セッションの開始・終了、貸出未完了数、指定外備品のスキャン履歴を確認する

  • 貸出時スキャンの団体、備品、日時、端末識別子、判定結果を確認する

  • 持出後の二重確認スキャンの対象備品、配置完了結果、操作元、端末識別子を確認する

メンテナンス:

  • 年度更新: 「全データをリセット(Reset for New Year)」ボタン(※確認ダイアログ2回表示)

    • EQUIPMENT_RESET保持者のみ実行可能

    • クリックでPOST /system/resetを実行する

    • 備品の状態、現在地、企画割当を初期化し、進行中の貸出セッションを終了扱いにする

    • 変更履歴、スキャン履歴、通知履歴、ラベル履歴は削除しない

Google SpreadsheetとのExport / Import用の画面とAPIは提供しない(Scope Out)。


9. 外部連携仕様​

9.1 申請システム​

申請システムから、企画ごと・備品種別ごとの必要数を取得する。取得した値はEquipmentRequestsへ保存し、備品割当案の入力として利用する。

連携項目:

  • 企画ID、所属団体、企画名

  • 備品種別

  • 必要数

  • 取込日時

運用ルール:

  • 申請数の取得に失敗した場合は、既存の確定済み割当を変更しない

  • 再取込は対象年度の全件スナップショットであることを明示した場合だけ原子的に反映し、各行の変更前後と取込結果をEquipmentRequestImportLogへ保存する。部分取得・通信失敗・対象年度不一致では現在値と確定済み割当を変更しない

  • 申請数の減少時はCONFIRMEDまたはLENTの割当を自動取消せず、過剰割当として警告する。企画がスナップショットから消えた場合も申請行・取込履歴・確定済み割当を物理削除せず、必要数0の履歴を残して新規割当案の対象外とする

  • 取込後に講義棟出展用備品を除外し、残りを同一教室優先で割り当てる

  • Sho-Roomから操作し、Service Bindingsで申請システムとの連携処理を行う(D-06で決定済み)

9.2 Discord API​

Discord APIは、学祭委員のログイン通知に使用する。団体識別用QRコードを出展者サーバーの各団体チャンネルへ配布する処理は別botが担当し、本システムの対象外である。

  • ログイン成功時は設定されたテキストチャンネル配下にプライベートスレッドを作成し、本人を追加してメンション付きメッセージを投稿する

  • プライベートスレッドの閲覧者は招待された本人とMANAGE_THREADS権限者に限定する

  • ログイン通知スレッドはメッセージ送信から15分後に削除し、作成・投稿・削除の結果をLoginNotificationLogへ記録する

  • 団体QRコードの発行・生成・配布、配布先チャンネルの管理、配布結果の記録は本システムでは行わない

  • 本システムのログイン通知Botには少なくともVIEW_CHANNEL、CREATE_PRIVATE_THREADS、SEND_MESSAGES_IN_THREADS、MANAGE_THREADSを付与する

9.3 プリンター(Epson TM-L90)​

基本情報​

機種: Epson TM-L90 Label Printer(LAN インターフェース。USB 接続端子は無い)

接続方式: 印刷用 PC の有線 LAN にケーブル1本で直結する。ルーター・スイッチは挟まない

通信方式: PC 上のラベル印刷エージェント(apps/label-print-agent)を介し、TCP 9100(RAW)へ ESC/POS を送る

  • 構築・運用手順は docs/files/guides/label-printer.md を正とする

  • 注意: FZ-N1からの直接印刷は仕様上不可(Webアプリからのみ印刷可能)

ネットワーク​

  • プリンターは IP を手動固定する(192.168.192.168/24、DHCP・APIPA は無効)。工場出荷時の IP と同じ値にし、初期化しても接続先が変わらないようにする

  • 印刷用 PC は有線側だけ 192.168.192.2/24 を固定し、ゲートウェイは設定しない。インターネット接続は Wi-Fi 側に残す

  • DHCP サーバーの無い直結で DHCP 設定のまま使うと、IP が決まるまで印刷できない。IP 固定時の UB-E04 の起動は約15秒

Mixed Content の扱い​

  • room.soshosai.com など HTTPS で配信する Sho-Room からブラウザが通信する先は、同じ PC のエージェント(http://127.0.0.1:15888)だけとする。ループバック宛ては Mixed Content の対象外

  • Chrome 142 以降は、公開サイトからループバックへの初回通信時に Local Network Access の許可を求める。ダイアログを開いただけでは問い合わせず、印刷操作の時点で初めて通信する

  • プリンターの LAN アドレスへブラウザから直接通信する構成、エージェントを別ホストに置く構成は採用しない

  • Safari での動作は確認していない。印刷用 PC では Chrome または Edge を使う

ラベル印刷エージェント仕様​

実装: Node.js(20 以上)の標準モジュールだけで書いた単一ファイル(agent.mjs)。依存パッケージを持たない

待ち受け: 127.0.0.1:15888 のみ

アクセス制御:

  • Origin が Sho-Room の各環境(本番・stg・preprod・dev1〜5)と http://localhost:5173 以外のブラウザからの要求は 403 とする。起動引数 --allow-origin で追加できる

  • Host が 127.0.0.1 / localhost 以外の要求は 403 とする(DNS リバインディング対策)

エンドポイント:

  • GET /health: { printer: "host:port", reachable: boolean, paperWidthDots: number } を返す。reachable はプリンターの 9100 番へ TCP 接続できたか

  • POST /print: 本文(application/octet-stream、上限 4MB)の ESC/POS をそのままプリンターへ送る。成功は 200、プリンターへ届かなければ 502 と理由(error)を返す。複数要求は届いた順に1件ずつ送る

設定: プリンターの IP(既定 192.168.192.168)、RAW ポート(既定 9100)、印字幅ドット数(既定 576 = 80mm 紙)は起動引数で与える。Web からプリンター設定を送る API は持たない

成功の意味: プリンターへ送り終えたことまでを表し、紙に出たことは確認しない

印刷データ形式​

  • Sho-Room がラベル全体を canvas に描画し、ESC/POS のラスター画像(GS v 0、256 行ごとに分割)へ変換して送る。先頭は ESC @、末尾は GS V 65 0(フルカット)

  • 文字・QR ともプリンター内蔵の機能を使わず画像にする。内蔵の漢字フォントは仕向けで有無が変わり、文字サイズは整数倍しか選べず、内蔵 QR は文字と横並びに配置できないため

  • 描画幅は GET /health の paperWidthDots に合わせる

印刷内容仕様​

印刷する情報:

  1. QRコード: 備品のUUID(Items.id)を格納

    • サイズ: 6(モジュール)

    • 誤り訂正: レベルM

    • 配置: 中央

  2. 備品種別: 大きな文字で表示

    • フォントサイズ: 48px

    • 太字: ON

    • 配置: 中央

  3. 元の場所: 元の場所: {元の場所}形式で表示

    • フォントサイズ: 24px

    • 配置: 中央

  4. 持出先: 持出先: {持出先}形式で表示

    • フォントサイズ: 24px

    • 配置: 中央

  5. 使用団体: 使用団体: {団体名}形式で表示

    • フォントサイズ: 20px

    • 配置: 中央

  6. 使用企画: 使用企画: {企画名}形式で表示

    • フォントサイズ: 20px

    • 配置: 中央

印刷トリガー​

UIでの使用:

  • Webアプリの備品編集モーダルから「ラベル印刷」ボタンをクリック

  • 押した時点で GET /health を呼び、エージェント未起動・プリンター未接続なら理由を表示して送信しない

  • 印刷成功/失敗をトースト通知で表示

年度更新時のラベル運用:

  • QRに格納する不変のItems.idは貼り替え後も同じ値を使用する

  • ラベル本文の持出先、使用団体、使用企画は年度の割当に依存するため、割当確定後の年度更新時に有効な備品を全数(2,000件超を想定)再印刷して貼り替える。個別の再発行と同様にLabelPrintLogへ記録する

  • 一括再印刷はWeb側のジョブとして実行し、印刷済み件数、失敗件数、未貼付件数をEQUIPMENT_EDIT保持者が確認できるようにする。年度更新でラベルを自動的に再利用したり、古い持出先を正しい情報として残したりしない

エラーハンドリング​

よくあるエラーと対処:

  • ラベル印刷エージェントに接続できません: エージェントが起動していない、またはブラウザのローカルネットワークへのアクセスが許可されていない。エージェントを起動し、サイト設定で許可する

  • プリンター (...) に接続できません: プリンターの電源・ケーブル、PC 有線側の IP 設定、ステータスシートの IP を確認する

9.4 大学環境cronサーバー向けエクスポート​

学祭準備期間に大学環境のcronサーバーが全備品データを取得するための連携である。

  • エンドポイントはGET /system/export/allとする

  • 認証はJWTではなく、専用のX-API-Keyで行う

  • 全備品データを一括で返し、ページネーションは行わない

  • APIキーは秘密情報として扱い、ログやレスポンスへ出力しない

  • 呼出頻度、取得データの保存先、保存期間は運用設計で定める

  • Cloudflareのバックアップ・リストアの代替とはしない


10. 運用・保守​

10.1 保守体制​

  • 管理チームが保守管理を担当する

10.2 サポート体制​

  • エラーログ、Discord通知の失敗、ラベル印刷の失敗、同期競合を外部ログシステムへ送信する

  • 利用者からの問い合わせと障害連絡は管理チームのDiscordで受け付ける

10.3 監視​

  • Cloudflareの稼働状況、アクセス状況、エラーを監視する

  • Discordログイン通知の削除処理はequipment-workerのScheduled Handler(Cloudflare Cron Trigger、1分間隔)が実行し、delete_atを経過したスレッドを処理して失敗と再試行を監視する

  • 貸出セッションの開始・終了漏れ、指定外備品のスキャン、貸出未完了を監視する

  • POST /auth/nfcは初期値として同一端末あたり10分間に5回までとし、超過時は429(code: TOO_MANY_REQUESTS)を返す。Discord通知はログイン処理と分離したキューで処理し、通知のレート制限でログインを失敗させない

  • ラベル印刷エージェントとEpson TM-L90のヘルスチェック、印刷失敗を監視する

10.4 制約条件​

  • 2026年度の蒼翔祭は2026/10/10、10/11に開催予定であり、それまでに開発、受け入れテスト、現場リハーサルを完了する

  • FZ-N1はAndroid 6.0.1(API Level 23)固定であり、対応ライブラリとSDKに制約がある

  • Epson TM-L90は印刷用PCに直結し、ラベル印刷エージェントを経由する。FZ-N1から直接印刷しない

  • 権限はDiscordロールから計算し、システム内で一時的に変更しても次回ログイン時に再計算する

  • login-api、discord-auth-worker、line-auth-worker、unified-auth-worker等の既存の共通認証基盤を利用し、本プロジェクトでは新規構築しない

  • FZ-N1で自由記述を必要とする運用は採用しない

  • ログイン通知用Discord Botには、対象チャンネルでプライベートスレッドを作成・閲覧し、投稿、メンバー追加、削除を行える権限を付与する

  • 団体識別用QRコードの発行・配布は別botが担当し、本システムはそのDiscordチャンネルや配布処理を管理しない

10.5 前提条件​

  • 学祭委員名簿は外部システムからsoshosai-system-dbへ供給される

  • 名簿には学籍番号とDiscord IDが保持され、両者を一意に対応付けられる

  • 学祭委員はDiscordサーバーへ参加し、ロールに応じた権限が計算される

  • ログイン通知を受ける学祭委員は、通知スレッドの親チャンネルを閲覧できる

  • 申請システムから企画ID、所属団体、企画名、企画別・備品種別別の必要数を取得できる

  • 別botが配布した団体QRコードから、対象団体を識別できる。QRのペイロードはOrganizations.idのUUIDを平文で格納し、署名・暗号化・有効期限は設けない。真正性検証を行わない設計判断であり、写真の使い回し等で別団体のセッションを開始できる可能性はあるが、セッション開始にはログイン済み学祭委員のFZ-N1が必要なため実害を限定する

  • 備品割当前に、講義棟出展で使用する備品または必要数を識別できる

  • 貸出時に団体の担当者が操作するFZ-N1と、教室で立ち会う学祭委員を用意できる

  • 貸出セッション開始時に、対象団体の当年度の確定済み割当リストをオンラインでFZ-N1へ事前ダウンロードできる。開始後に通信が切れても、端末内のリストで貸出時スキャンを照合できる

  • ラベル印刷環境には、ラベル印刷エージェントが稼働するPCとTM-L90がLANケーブルで直結されている

10.6 決定ログ​

本システムの要確定事項と、その決着状況を一覧で管理する。表の形式は全システム共通(.agents/skills/edit-specification/SKILL.md「決定ログの書式」を参照)。

状態 は 未確定 / 保留 / 決定済み / 対象外 のいずれかを取る。

テント等のレンタル業者(スパイス)との回収タイミング交渉が完了していないため、D-02〜D-05の最終決定は保留する。回答期限を2026/09/11とし、期限までに回答がない場合はパターンBを実装・当日運用の既定とする。交渉結果に応じてパターンAまたはパターンBのいずれかへ確定し、参加団体による返却時のセルフスキャンは行わない。

ID分類判断が必要な内容状態期限結論
D-01企画認証の有効期限企画認証コードおよびWeb上の参加団体向け認証セッションは本システムで扱わない対象外—対象外(決定不要)。団体QRコードの発行・配布は別botが担当する
D-02返却成立条件スパイスの回収タイミングに応じ、講義棟前へ返却された時点を返却成立とする運用をどのパターンで採用するか保留2026/09/11期限までに回答がない場合はパターンBを既定とする。パターンAは2026/10/12に講義棟前へ返却された時点、パターンBは例年通り講義棟前に返却された時点とする。パターンBが学生課等から承認されるかは未確定
D-032日目夜の記録スパイスの回収タイミングに応じて、2日目夜のスキャン・記録を行うか保留2026/09/11パターンAは記録しない(2日目夜はスキャン・記録を行わない)。パターンBは講義棟前到着時に全数スキャンを行う
D-04中間集積場所講義棟前へ一時集約する場合、現在地として記録するか保留2026/09/11パターンAは記録しない。パターンBは混乱防止のため講義棟前を現在地として記録する
D-05足拭き・教室収容スパイスの回収タイミングに応じて、誰が、いつ、どの状態更新とともに実施するか保留2026/09/11パターンAは講義棟前到着次第に逐次スキャン・足拭き・教室収容(可能であれば搬入した各団体自身が足拭き・教室搬入)。パターンBは片付け後に足拭き・教室収容し、その後全数スキャン
D-06申請システム連携API、定期取込、ファイル取込のどの方式を利用するか決定済み—Sho-Roomから操作し、Service Bindingsで処理
D-07状態遷移貸出状態と備品状態の正式な値、遷移、操作権限決定済み—本仕様書§4.6の遷移表を実装・テストの基準として正式採用する。D-02〜D-05の結論とは独立
D-08性能目標値一覧検索、スキャン照合、一括操作の応答時間目標未確定2026/09/112026/09/11までに性能試験計画でp95目標を確定する
D-09Discord通知先親チャンネル、スレッド名の形式、通知本文、SYSTEM_MANAGE保持者向け失敗通知先未確定2026/09/042026/09/04までにAPI設計で通知先と本文を確定する
D-10共通認証基盤の認証・セッション有効期限login-api等の共通認証基盤の既存機構で定義するため、本システムでは扱わない対象外—対象外(共通認証基盤の管轄)

10.7 リスク・対応方針​

ID内容影響度対応方針
R-01複数端末による同一備品の競合中操作IDによる冪等化、Items.revisionとbase_revisionによる競合判定、競合記録、Sho-Roomでの確認を実装する。端末時計や秒精度のupdated_atで勝敗を決めない
R-02Android 6.0.1の通信・SDK制約中TLS 1.2、カメラ、Toughpad SDKを早期検証する
R-03ラベル印刷エージェント停止により印刷できない低ヘルスチェックと再試行手順を用意する
R-04団体の担当者が貸出時の備品スキャンを行わない高学祭委員が教室に常駐し、未確認の割当数を可視化して現場で確認する。セッション終了時も未スキャンの割当済み備品をALLOCATEDのまま残す
R-05誤った備品を持ち出す高オンライン時はサーバー、オフライン時は事前取得した割当リストで即時照合して警告し、指定外スキャンを監査ログへ残す。オンライン復帰後にサーバーで再検証する
R-06ラベルの重複発行・貼付中発行履歴、再発行警告、貼付後確認で検知する
R-07返却時のスキャン・収容が時間内に終わらない高スパイスとの交渉結果に応じてパターンA/Bを確定する。パターンAは2日目夜の記録を行わず到着後に逐次処理し、パターンBは講義棟前到着時と教室収容後に全数スキャンを行う
R-08特定担当者・端末へ作業が集中する高教室ごとの立会い担当とFZ-N1の受け渡し手順をリハーサルする
R-09自動割当が現場の条件を満たさない中確定前レビュー、手動修正、不足・分散配置の明示を行う
R-10ログイン集中時にDiscord API制限やアクティブスレッド上限へ達する中POST /auth/nfcを同一端末あたり10分5回に制限し、通知を非同期キュー(最大同時10件)で処理する。API応答を監視し、再試行、送信15分後の削除を行う。通知失敗でログインを止めない
R-11Discordの通知設定により本人へプッシュ通知されない中メンション付き投稿までを保証範囲とし、FZ-N1にもログイン成功を表示する
R-12Botの権限不足によりスレッドを作成・削除できない中起動時または定期ヘルスチェックで必要権限を検査し、SYSTEM_MANAGE保持者へ通知する
R-13共通認証基盤の認証トークン管理対象外login-api、discord-auth-worker、unified-auth-worker等の認証・セキュリティ機構の管轄。本システムでは扱わない
R-14共通認証基盤・既存Web/BFFのCSRF対策対象外login-api等の共通認証基盤と既存Web/BFFの対策の管轄。本システムでは要件を定義しない
R-15共通認証基盤のログアウト後の認証状態対象外login-api等のCookie削除・セッション失効およびunified-auth-workerの検証の管轄。本システムでは扱わない

11. テスト方針​

11.1 Web App​

  • 開発環境: wrangler dev --remoteで動作確認する

  • テスト環境: concurrencyを使用したHono/React同時実行環境で検証する

  • テスト項目: 各機能の動作、バリデーション、権限別の表示・操作、レスポンシブデザイン、BFFによるJWT中継、ラベル、割当、エラーハンドリングを確認する

11.2 FZ-N1アプリ​

  • ビルド: ./gradlew clean assembleDebugで毎回ビルド・検証する

  • テスト項目: 団体QRスキャン、備品QRの逐次スキャン、持出後の二重確認スキャン、NFCログイン、貸出セッションの開始・終了、指定外備品への警告、Discord通知結果の表示、物理ボタン、オフライン操作・同期、競合記録、API連携、エラーハンドリング、音声フィードバックを確認する。返却は2026/09/11に確定したパターンだけを実装・受入試験の対象とし、期限までに回答がない場合はパターンBを対象とする

11.3 受け入れ基準​

  • 2,000件以上の備品が登録された状態で、全件取得せずに検索、絞り込み、ソート、ページ移動ができる

  • NFCログインで学生証から読み取った学籍番号とIDmが同一スタッフの名簿情報に照合され、名簿未登録・IDm不一致が適切に拒否される

  • ログイン成功後、設定されたDiscordチャンネル配下にプライベートスレッドが作成される

  • ログインした本人がスレッドへ追加され、対象者を限定したメンション付き通知が投稿される

  • 通知スレッドを本人、Bot、MANAGE_THREADS権限者以外が閲覧できない

  • 通知本文とスレッド名に学籍番号、カード情報、JWT等の機密情報が含まれない

  • 通知メッセージ送信から15分経過後、次の1分間隔の削除判定で通知スレッドが削除される

  • メッセージ投稿に失敗した場合も、スレッド作成から15分経過後の削除判定で空スレッドが削除される

  • スレッド削除の成功または失敗結果がDBへ記録される

  • Discord通知に失敗してもNFCログインは完了し、通知失敗がSYSTEM_MANAGE保持者から確認できる

  • 個別スキャンで備品を照会し、選択式で貸出状態・備品状態を更新できる

  • 通常の団体貸出が団体QRを起点とする貸出セッション方式で行われ、FZ-N1の貸出スキャン画面から割当一致を確認してセッションを終了できる

  • 団体一括操作は通常の団体貸出の代替にならず、EQUIPMENT_BULK_OPERATION保持者が理由付きの例外処理としてだけ実行できる

  • PUT /items/:idにstatusを指定しても更新されず、code: VALIDATION_ERRORで拒否される。貸出状態の変更が§4.6の各専用経路以外から行えない

  • 貸出セッション終了時に一致したLENDING_SCANNEDだけがLENTへ遷移し、未スキャンの割当済み備品はALLOCATEDのまま残る

  • オフライン操作が再送時に二重反映されず、競合が履歴として確認できる

  • 申請システムの必要数を用い、講義棟出展分を除外した割当案を作成できる

  • 同一企画の備品について、割当数・同一教室への集約数・分散先・不足数が画面に表示される

  • 教室に監視の学祭委員が常駐し、参加団体の担当者が来室した状態で貸出を開始できる

  • 学祭委員がFZ-N1で団体QRコードを読み取り、対象団体の貸出セッションを開始できる

  • 団体QRコードはOrganizations.idのUUIDを平文で格納し、署名・暗号化・有効期限を設けず真正性検証を行わないこと、別botから配布され本システムが発行・生成・配布を行わないことが仕様上明記されている

  • 学祭委員がログイン済みのFZ-N1を団体の担当者へ手渡し、担当者自身が対応する備品のQRコードを一つずつスキャンできる

  • セッション開始時に取得した対象団体の当年度の確定済み割当リストを端末へ保存し、オンライン時はサーバー、オフライン時は端末内リストでスキャンの一致を即時判定できる

  • 指定外の備品をスキャンした場合にFZ-N1で警告を表示し、持ち出し完了にはしない

  • 一致・不一致にかかわらず、スキャンした団体、備品、日時、貸出セッション、割当先企画、認証上の操作元、実際のスキャン担当区分、端末識別子、source、判定結果を確認できる

  • 各団体の搬出完了後、学祭委員(備品PJ)がFZ-N1を持って各テント等を巡回し、対象備品の二重確認スキャンを開始できる

  • 二重確認スキャンは、学祭委員またはその場に居合わせた参加団体の担当者が備品を一つずつ読み取り、配置完了を確認・確定できる。実際のスキャン担当を片方に固定せず、参加団体担当者に別の認証を要求しない

  • 二重確認スキャンの結果が貸出時スキャンと区別され、対象備品、配置完了結果、操作元、端末識別子とともに履歴から確認できる

  • 担当者からFZ-N1が返却されたとき、または学祭委員が明示的に終了したときに貸出セッションを終了できる

  • 貸出セッションの対象団体ID、開始日時、終了日時、タイムアウト日時、開始した学祭委員ID、終了操作元、端末識別子が記録され、同じ端末・団体の未終了セッションを同時に開始できない

  • 貸出セッション中は貸出スキャン画面以外への遷移、設定操作、ログアウト、通常の戻る操作が無効になり、アプリ再起動後もこの制約が維持される

  • 備品の全項目について変更前後の値、実行者、日時、操作元、端末識別子が記録される

  • Sho-Roomで備品の変更履歴とスキャン履歴を検索・閲覧できる

  • ラベルへQR、元の場所、持出先、使用団体、使用企画が文字で印刷され、記号による行き先表現がない

  • ラベルの重複発行時に警告され、再発行理由と貼付後確認の結果が記録される

  • 学祭倉庫のコーン、バケツ、消火器等を通常の備品と同様に管理できる

  • 「学園祭実行委員」DiscordロールにEQUIPMENT_VIEWが付与され、備品閲覧、FZ-N1メインメニュー、通常の団体貸出セッション、貸出・配置確認スキャンを利用できる

  • Discordロール名や固定的なロール階層ではなく、roleConfig.tsから計算されたJWTの権限ビットでSho-RoomとFZ-N1の各機能へのアクセスが制御される

  • Sho-Room Workerの既存BFFを経由した備品管理API要求で、BFFが共通認証基盤(login-api等)が設定したsoshosai_staff_jwt CookieのJWTをAuthorizationヘッダーへ変換して付与する

  • 年度更新により現在状態と割当が初期化され、進行中の貸出セッションが終了扱いになり、過去の貸出セッションと監査ログは保持される

  • 外部エクスポートAPIが専用APIキーで認証され、正しいデータを返す

  • D-02〜D-05はスパイスとの回収タイミング交渉結果待ちの保留事項として、パターンA/Bの両方が関連する設計書と試験項目へ反映されている


12. 権限ビットと機能の対応​

備品管理機能の認可は、JWTのpermissionsに含まれる権限ビットで判定する。packages/api/auth-workers/discord-auth-worker/src/roleConfig.tsがDiscordロールIDと権限ビットの対応に関する正本であり、複数のDiscordロールを持つ場合は各ビットをORで合成する。権限定数とビット値の正本はpackages/shared/src/permissions.tsとする。アプリケーション内には、従来の固定的な4段階ロール区分を設けない。

「学園祭実行委員」DiscordロールにはEQUIPMENT_VIEWを付与する。このロールを持つ学祭委員は、備品の閲覧、FZ-N1メインメニュー、通常の団体貸出セッション、貸出・配置確認スキャンを利用できる。編集、削除、マスタ管理、一括操作、年度更新、特殊操作は、それぞれ対応する別の権限ビットが必要である。

必要な権限・認証対象機能補足
EQUIPMENT_VIEW備品一覧・詳細・検索・ダッシュボード・履歴の閲覧、マスタ選択肢の参照、FZ-N1メインメニュー、通常の団体貸出セッション、貸出・配置確認・返却スキャン「学園祭実行委員」Discordロールへ付与する。参加団体担当者がFZ-N1を操作する場合も、ログイン済み学祭委員のJWTに紐付ける
EQUIPMENT_EDIT備品登録・編集、個別スキャンからの状態変更、備品状態の強制変更、申請数取込、割当案作成・修正・確定、ラベル発行・貼付後確認EQUIPMENT_VIEWだけでは備品情報や状態を任意変更できない。PUT /items/:idでは貸出状態を変更できず、§4.6の各遷移は専用の操作経路を使う
EQUIPMENT_DELETE備品の論理削除削除後も監査履歴を保持する
EQUIPMENT_MANAGE_LOCATION場所マスタの登録・編集・無効化閲覧はEQUIPMENT_VIEWで行う
EQUIPMENT_MANAGE_ORGANIZATION団体マスタの登録・編集・無効化閲覧はEQUIPMENT_VIEWで行う
EQUIPMENT_MANAGE_TYPE備品種別・企画マスタの登録・編集・無効化閲覧はEQUIPMENT_VIEWで行う
EQUIPMENT_BULK_OPERATION団体一括操作(例外処理)通常の団体貸出の代替には使用しない
EQUIPMENT_INTEGRATIONJWT認証を用いる備品連携処理現行の大学環境cron向けエクスポートAPIはこのビットではなく専用APIキーで認証する
EQUIPMENT_RESET年度更新実行前に二重確認し、履歴を削除しない
SYSTEM_MANAGE学祭委員の権限確認、権限変更監査・通知失敗の確認、貸出セッションの強制終了通常の備品編集・状態変更・一括操作・年度更新を自動的に許可するビットではない
専用APIキー大学環境cron向け全データエクスポートJWTと人の権限ビットは使用しない

権限不足時の動作:

  • HTTPステータスコード: 403 FORBIDDEN

  • 機械可読コード: FORBIDDEN

  • エラーメッセージ: “この操作を実行する権限がありません”

  • UI: 該当機能のボタン・メニューを非表示にする

注意事項:

  • 認可処理ではDiscordロール名を直接比較せず、必要な権限ビットを検査する

  • ALL_PERMISSIONSはすべての権限ビットをORした値だが、別の抽象ロール階層を意味しない。roleConfig.tsでALL_PERMISSIONSを付与されたDiscordロールも、各機能では同じ権限ビット検査を通過する

  • 外部エクスポートAPIは人の権限マトリクスの対象外であり、大学環境のcronサーバーがX-API-Keyで認証して利用する。JWT利用者の権限として扱わない

  • 参加団体の担当者にはSho-RoomやAPIの認証情報を与えず、学祭委員がログインしたFZ-N1を貸出中だけ一時的に操作させる

  • Google SpreadsheetのImport / Exportは対象外であり、権限ビットも割り当てない


13. 用語集​

用語説明
蒼翔祭(そうしょうさい)会津大学学園祭の正式名称
学祭委員蒼翔祭実行委員。本システムの管理・現場利用者
参加団体蒼翔祭へ出展し、割り当てられた備品を使用する団体
企画参加団体が申請する出展単位。同一団体が複数持つ場合がある
備品貸出、返却、所在管理の対象となる物品
備品種別長机、椅子、コーン、バケツ、消火器等の分類
元の場所備品を通常保管している教室または倉庫
持出先蒼翔祭期間中に備品を使用する場所
割当特定の備品を企画および持出先へ対応付けること
団体QRコードOrganizations.idのUUIDを平文で格納した団体識別QRコード。署名・暗号化・有効期限はなく、真正性検証は行わない。別botが出展者サーバーの各団体チャンネルへ配布し、本システムは発行・生成・配布を行わない
貸出時スキャン学祭委員立会いのもと、団体の担当者がFZ-N1で持ち出す備品のQRを一つずつ読み取る操作
二重確認スキャン各団体の搬出完了後、学祭委員(備品PJ)が巡回するテント等で、学祭委員またはその場に居合わせた参加団体の担当者がFZ-N1で対象備品を一つずつ読み取り、配置完了を確認・確定する操作
貸出セッションFZ-N1上で団体QRコードの読み取りから、担当者からの端末返却または学祭委員による明示的終了まで管理する団体単位の貸出記録
出展許可証参加団体に発行されるQRコード付き許可証
FZ-N1現場で使用するPanasonic製Android端末(Android 6.0.1)
Sho-Room学祭委員が使用する統合管理Webツール
ログイン通知スレッドNFCログイン成功時にDiscord上へ一時作成し、本人へ利用情報を通知するプライベートスレッド
JWT認証トークン(JSON Web Token)
端末識別子FZ-N1など、操作に使用した端末を監査記録へ紐付ける識別情報
スキャン担当区分実際に端末を操作した人の区分。学祭委員はSTAFF、参加団体の担当者はORG_REPRESENTATIVEとしてScanLog.scanned_by_roleへ記録する
BFFSho-Room Workerが共通認証基盤(login-api等)が設定したCookieからJWTを取り出し、Authorizationヘッダーへ変換して備品管理APIへ中継するBackend for Frontend
UUID v7時系列でソート可能なユニークID。新規エンティティの主キーに使用し、ItemHistory.id、SyncLog.id、AuthorityChangeLog.idは既存の整数AUTOINCREMENTを維持
ラベル印刷エージェント印刷用PC上で動き、Sho-Roomから受け取ったESC/POSをTM-L90へ中継するスクリプト(apps/label-print-agent)

14. 付録​

14.1 QRコード仕様​

  • 形式: QRコード

  • 内容: 変更されない備品UUID(Items.id)

  • 生成: Epson TM-L90で印刷

  • 併記情報: 元の場所、持出先、使用団体、使用企画を人が読める文字列で印刷する

  • サイズ: 2cm × 2cm程度

14.2 団体識別QRコード仕様​

  • 形式: QRコード。ペイロードはOrganizations.idのUUID文字列を平文で格納する

  • 発行・配布: 出展者サーバーの各団体チャンネルへの配布を含め、別botが担当する。本システムは発行・生成・配布を行わない

  • 真正性検証: 署名、暗号化、有効期限を設けず、真正性検証を行わない。写真の使い回し等で別団体のセッションを開始できる設計上のリスクを許容するが、開始にはログイン済み学祭委員のFZ-N1が必要であり、実害は限定的と判断する

  • 利用: 学祭委員がログイン済みのFZ-N1で読み取り、対象団体の貸出セッションを開始する

  • 保存: QRコードの発行・配布情報や配布先チャンネルを本システムのデータとして保存しない

14.3 システム構成フロー図​

14.4 データフロー図​

14.5 付録A: 現場運用想定​

貸出後の二重確認スキャン​

各団体が対象備品をテント等の設置場所へ搬出完了した後、次の手順で配置完了を確認・確定する。

  1. 学祭委員(備品PJ)がFZ-N1を持って各テント等を巡回する

  2. 学祭委員またはその場に居合わせた参加団体の担当者が、FZ-N1で対象備品を一つずつスキャンする

  3. 対象備品の配置完了を確認・確定する。実際のスキャン担当は運用で決め、参加団体担当者に別の認証は要求しない

返却運用(D-02〜D-05)​

テント等のレンタル業者(スパイス)との回収タイミング交渉結果により、次の2パターンのいずれかに最終確定する。交渉結果が確定するまでD-02〜D-05は保留し、参加団体による返却時のセルフスキャンは行わない。

項目パターンA:スパイスが回収を遅らせてくれる場合パターンB:スパイスが例年通り回収する場合
D-02(返却成立条件)片付けの日(2026/10/12)に講義棟前へ返却された時点を返却成立とする昨年通り、講義棟前に備品が返却された時点を返却成立とする。ただし、この運用が学生課等から承認されるかは未確定
D-03(2日目夜の記録)記録しない。2日目夜はスキャン・記録を行わない講義棟前到着時に全数スキャンを行う。到着時点で備品への責任が備品課に移るため
D-04(中間集積場所を現在地として記録するか)記録しない混乱防止のため、講義棟前を現在地として記録する
D-05(足拭き・教室収容)備品課・学祭委員が講義棟前へ到着次第、逐次スキャン・足拭き・教室収容を行う。可能であれば、備品を持ってきた各団体自身に足拭き・教室搬入まで行ってもらう運用を志向する備品課・学祭委員がテント等の片付け後に足拭きを行って教室へ収容し、その後に全数スキャンを行う

変更履歴​

日付バージョン変更内容
2026-09-233.6Issue #982対応。TM-L90をPCに直結し、Electron製e-POS Bridgeをラベル印刷エージェント(ESC/POS素通し)へ置き換え。§9.3を全面改訂
2026-09-023.5Issue #675対応。PUT /items/:idからstatusを除外し、貸出状態の変更を§4.6の遷移表に対応する専用経路のみに限定
2026-09-023.4Issue #668対応。備品状態の強制変更をEQUIPMENT_EDIT、権限確認・監査等の残りの特殊操作をSYSTEM_MANAGEに整理
2026-09-023.3固定的な4段階ロール区分を廃止し、roleConfig.tsを正本とするDiscordロール別の権限ビット付与方式へ統一。「学園祭実行委員」ロールのEQUIPMENT_VIEW付与方針を反映
2026-08-313.2スパイスとの回収タイミング交渉待ちのD-02〜D-05をパターンA/Bの保留事項として反映し、団体搬出後のテント等での二重確認スキャンを貸出フロー・監査記録・付録へ追加
2026-08-303.1プロジェクトリーダー確認済みの前日団体貸出フローを反映。団体QRによるFZ-N1貸出セッション、担当者による備品スキャン、指定外警告、セッション記録を追加し、参加団体向けWeb認証・コード関連機能を対象外化
2026-08-303.0Notion要件定義書 v0.5.0を反映。Google Spreadsheet連携、備品割当、Discordログイン通知、Sho-Room BFFセッション、監査・ラベル・オフライン要件を整理
2026-02-032.8Notion対応: すべての箇条書き(- / 1.)の前に空行を追加。e-POS Bridge仕様を詳細化(JSON印刷形式、エンドポイント、実装例、エラーハンドリング)。TM-L90にUSBポートが存在しない旨を明記
2026-02-032.7BFFパターン(Backend for Frontend)を採用。Cloudflare Pages + Pages Functionsでセキュアな認証を実装。HttpOnly Cookieによる XSS対策。ドメイン構成の明記。Vitestによるテスト仕様を追加。厳密なファイル分割規則を策定
2026-02-032.6学年・学生身分の情報を削除。OTP方式への変更に伴い不要になったAPI・UIを整理。StaffsテーブルとAPIレスポンスから該当フィールドを削除
2026-01-312.5権限変更機能を追加。学祭委員一覧取得API、NFCスキャンによる検索API、権限変更API、AuthorityChangeLogテーブルを追加。Web特殊操作メニューに権限変更機能を実装
2026-01-312.4A2ボタンを概要/詳細切替に使用。物理ボタンの動的割り当て追加。特殊操作機能(IDm再登録、強制状態変更)を追加。AppButtonManager APIに更新
2026-01-312.3FZ-N1の物理ボタン(A1-A3)をToughpad SDKで制御する仕様を追加。スライド操作から物理ボタン操作に変更。画面UIを物理ボタン対応に更新
2026-01-312.2初回登録をCampus Square方式からOTP方式に変更。Discord QRログイン削除。Zod/standard-validatorによるバリデーション追加。学祭準備期間用データエクスポートAPI追加。独自テンキーUI実装。FZ-N1からのプリンター操作を不可に変更(Webのみ)
2026-01-312.1NFCログイン成功時の確認画面を追加。団体許可証→出展許可証に用語変更
2026-01-312.0元仕様書を基に全面改訂。会津大学学園祭向け仕様を明記、NFCログイン詳細、一括操作、Mermaid図、SQLスキーマを追加
2026-01-311.1非機能要件、権限管理、バリデーション、エラー処理、テスト方針を追加
2024-XX-XX1.0初版作成