WebアプリをClaudeCodeで作成しました!④

全5回 ― 第4回

要求:イベントとお知らせに画像を入れたい

クライアントから「イベントの告知に画像を入れたい」という要望が来ました。

最初の試み:Firebase Storage → 壁にぶつかる

まず候補に上がったのが、すでに使っているFirebaseのStorageサービス。しかし確認するとこんなメッセージが:

「Storage を使用するには、プロジェクトの
 料金プランをアップグレードしてください」

Firebase Storage は無料プラン(Spark)では使えないことが判明。有料プラン(Blaze)への移行が必要でした。

代替案の検討

AIに相談すると3つの選択肢を提示してくれました。

方法概要コスト
Cloudinary専用の画像ホスティングサービス無料枠25GB
Base64でDB保存画像データをそのままFirebaseに保存無料だがDBが重くなる
URL手入力別サービスに上げてURLを貼る無料だが手間

Cloudinaryを選択。 無料枠が十分大きく、管理画面から直接アップロードできるのが決め手でした。

ブラウザ内で圧縮してからアップロード

「5GBはすぐ上限に達しないか?」という心配に対して、ブラウザ内で圧縮する方法を実装しました。

async function compressImage(file, maxWidth = 1200, quality = 0.82) {
  // Canvas APIで長辺1200px・JPEG 82%品質に変換
  // 結果:100〜200KB程度に圧縮
}

試算すると:

5GB ÷ 200KB = 約25,000枚
週2枚アップ → 約240年分

実質無制限に近い感覚で使えます。

Cloudinaryでの設定手順

  1. cloudinary.com で無料アカウント作成
  2. Settings → Upload → Upload presets → Add upload preset
  3. Signing Mode を「Unsigned」に設定(ここ重要)
  4. Cloud Name と Preset Name をメモ

失敗談①:プリセット名にバッククォートが入っていた

エラー:Upload preset not found

プリセット名をバッククォート付きで登録していたため、コード内の文字列と一致しませんでした。Cloudinaryで名前を修正して解決。プリセット名は英数字のみが安全です。

失敗談②:画像が保存後しばらくすると消える

アップロードは成功しているのに、時間が経つと画像が消えるという謎の不具合。原因は normalize関数 でした。

// ❌ Before:imageUrlが除外されている
.map(a => ({ text: a.text.trim(), postedAt: a.postedAt || '' }));

// ✅ After:imageUrlを保持
.map(a => ({
  text: a.text.trim(),
  postedAt: a.postedAt || '',
  imageUrl: a.imageUrl || ''   // これを忘れていた
}));

Firebaseからデータを読み込む際に整形する関数が、imageUrl を除外していました。何か保存操作をするたびに imageUrl が消えるという動作になっていました。

新しいフィールドを追加したら、normalize関数の更新を忘れずに。 これは今回の開発で一番ハマったポイントです。ここまで内容見てもさっぱり分からないと思っても全然大丈夫です。私もさっぱりわかっていません。AIが勝手にやってくれていますので論理的な指示の出し方の方が重要だからです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です