ノート共有アプリの体験を決めるのは、地味ですがファイルアップロード周りです。
メディカルサークルでは PDF・画像・ZIP の3形式、1ファイル最大50MBを前提に、複数ファイルを 1 つのノートに束ねて投稿できるようにしました。
本記事では、その実装で踏んだ判断と工夫をまとめます。
目次
複数ファイル統合と進捗の非同期制御
1 つのノートに複数ファイルを束ねる仕様のため、ファイルごとに「オリジナル保存 → プレビュー生成 → URL 取得」を直列で回す必要があります。
そのうえで、各ファイルの進捗を snapshotEvents.listen で拾い、「2/5 件目 68%」のような粒度で UI に反映しました。
非同期制御としては最も複雑な部分でしたが、ここを丁寧に作ったことで、大きいファイルでも体感が悪化しないアップロード体験を実現できています。
プレビューはアプリ内描画、画像は事前圧縮で軽くする
PDF は端末側のアプリ内レンダリングライブラリでページ単位に描画し、ピンチ操作の拡大や全画面プレビューに対応しました。
画像はアップロード時に端末側で圧縮した「プレビュー版」を Storage に保存し、一覧表示ではそのプレビューをキャッシュ付きで読み込みます。
オリジナル画像を一覧に出すと通信量も描画コストも跳ね上がるため、表示用と原本用を分けるのは早い段階で決めた方針です。
Storage のパス設計でアクセス境界を作る
Storage のパスは、プレビュー / オリジナル / ユーザーID / ノートID で構造化しました。
プレビューは認証済みユーザー全員に読み取りを許可し、オリジナルはクライアントからの直接読み取りを禁止しています。
オリジナルへのアクセスはサーバー経由でのみ可能にすることで、「閲覧」と「原本保護」を物理的に分離しました。
後から「有料会員のみ原本ダウンロード可」のような制御を加えやすいのも、この分離のおかげです。
アプリ側とセキュリティルールでバリデーションを二重化する
ファイル形式とサイズのチェックはアプリ側と Storage セキュリティルールの両方で行っています。
アプリ側は、ファイル選択時に拡張子(PDF・画像・ZIP)とサイズ(50MB 上限)を確認し、不適合ならアップロードボタンを押す前にエラー表示で弾きます。
サーバー側は、コンテンツタイプの正規表現マッチとサイズ上限を Storage ルールで二重に制限し、不正なクライアントからの直接アップロードに備えています。
アカウント削除時の関連データを安全に消す
退会フローでは、Storage の実体と Firestore のメタデータ、そして関連データ(コメント・いいね・フレンド関係・ブロック・非表示・下書き・時間割)をまとめて消す必要があります。
まず投稿ファイルを列挙し、Storage 実体(プレビュー+オリジナル両パス)を削除してから Firestore のメタデータを削除します。
続いて関連データを一括で物理削除し、最後にユーザードキュメントを論理削除して「退会済みユーザー」に置換、認証アカウントを物理削除する順序です。
途中で失敗しても再実行できる設計を意識しています。
まとめ
アップロードまわりは「動くこと」と「気持ちよく動くこと」の差が大きく、見えにくい部分ほど開発工数を食いやすい領域です。
メディカルサークルでは、進捗 UI・プレビュー生成・パス設計・二重バリデーション・退会時クリーンアップまでをワンセットで設計しました。
結果、医学部生が安心してノートを預けられる土台を作ることができました。

.webp%3Falt%3Dmedia%26token%3D6ca2c2ef-9413-4453-b992-55b66b11ed54&w=3840&q=75)


.webp%3Falt%3Dmedia%26token%3D900f385d-12a2-449b-8d1e-83a57cef0088&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D0e802fb0-2dda-44a7-bf80-5d39019635ba&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D3fb3dc66-ecca-402e-8fb8-fbec9407f7f5&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3Ddb21d760-e1ed-4ec2-af28-3462041e31b5&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3Dcce7bd72-f11e-4292-86bf-e6ccf3e7bf32&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D457ff920-e0df-4ff5-95eb-e29f74b73823&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3Dc21fcc77-7404-458d-9eb5-85b8d84ae1bc&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D92052f12-5280-49df-877a-b514582e95db&w=3840&q=75)

.webp%3Falt%3Dmedia%26token%3Da7c14698-1b08-4fea-89c6-f77a9121f4c5&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D899eeefd-f4c9-44a6-9ec2-3ced0b223ffd&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3Dca25fa6b-e233-43f7-90c3-e68e4c5b0bc5&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D7f18e5f1-cfda-4148-ab86-b3d2e6547262&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D5f10e078-4d87-4c87-928c-21b719cbf1cb&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D957b18b6-9b01-4c94-9207-7b9fca22a787&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3Dd952e11d-4461-47ae-892d-622fc3f2a48a&w=3840&q=75)
.webp%3Falt%3Dmedia%26token%3D532bb657-5670-49b4-9165-5f758062d8dd&w=3840&q=75)