セッション開始:2026/06/09 02:07
3D間取りの新規作成で、入力画像(間取り図)を サーバーアクション経由でアップロードしていた方式から、 クライアントから S3 へ直接 PUT(presigned URL 方式)へ移行する。 サーバーアクションのリクエストボディに base64 画像を載せて送る構造をやめ、画像の往復をクライアント↔S3に閉じることでサーバーアクションの負荷とペイロードを削減する。 あわせて、アップロード前にクライアント側で画像を圧縮し、アップロード量と後段の生成入力サイズを安定させる。
やること
inputImageUri のみを createSolidFloorPlan に渡すcreateSolidFloorPlan の入力を inputImage(base64)→ inputImageUri(文字列)へ。アップロード・ハッシュ計算をアクションから除去need-annotation-in-image-switch/_actions/ 配下にあった専用アクションを apps/user/actions/s3/get-upload-url.ts へ集約し、引数を { key, image: { mimeType } } に統一PhotoField に browser-image-compression を導入(maxSizeMB: 1 / maxWidthOrHeight: 1024 / fileType: "image/jpeg")ReplacementCreateForm → SolidFloorPlanCreateForm へ改名やらないこと
PhotoField から撮影経路(ImageCapture 等)を削除。今回はファイル選択のみgetUploadUrl の key 検証:今回は未対応(今後対応したい)→ 課題へ切り出しimage/jpeg へ統一するため透過は非対応(画像サイズ安定を優先・許容)lib/aws/s3/upload(base64→バイナリ保存)の削除(他用途で温存)PhotoField でファイル選択 → browser-image-compression で JPEG へ圧縮 → base64 文字列として ImageData{ mimeType, data } を state に保持key = solid-floor-plans/${MD5(data)}.${ext} を計算(コンテンツアドレス)getUploadUrl({ key, image: { mimeType } }) で presigned PUT URL を取得(有効期限 5分)Blob にデコードして S3 へ PUT(content-type は presigned の ContentType と一致させる)inputImageUri = ${AWS_CLOUDFRONT_ENDPOINT}/${key} を組み立て、createSolidFloorPlan({ inputImageUri }) でレコード作成🔴 重大バグ:PUT のボディが base64 テキストのまま(修正済み)
body: inputImage.data は base64 文字列であり、S3 は base64 を自動デコードしないため、保存される実体が「JPEG バイナリ」ではなく「base64 テキスト」になっていた。
旧サーバー実装 lib/aws/s3/upload は Buffer.from(data, "base64") でデコードしてから保存していた点が落ちていた。
誤検知に注意:作成フォーム内のプレビューはメモリ上の base64 を直接表示するため、S3 の実体が壊れていても正常に見える。
またファイル名がコンテンツアドレス(MD5(data))のため、旧実装が保存した正常な JPEG と新実装の壊れたオブジェクトがキー衝突し、S3 の中身を確認しても旧オブジェクトを見てしまう罠があった。
実際に aws s3 cp ... - | head -c 32 | xxd で先頭バイトを確認し、新規キーが 2f 39 6a 2f(/9j/...)=base64 テキストになっていることで確定した。
修正:body: await (await fetch(`data:${mimeType};base64,${data}`)).blob() で base64 を Blob にデコードしてから PUT。再アップロード後、先頭が ff d8 ff e0 ... JFIF =正常な JPEG になることを確認。
🟠 カメラ撮影は圧縮対象外だった → 撮影経路ごと削除
圧縮はファイル選択経路のみに入っており、カメラ撮影経路は無圧縮だった。指摘を受け、今回は撮影経路自体を削除して入力をファイル選択に一本化した。
🟡 getUploadUrl がクライアント指定の key を無検証で受理
認証済みユーザーならバケット内の任意キーへ presigned PUT URL を取得でき、任意オブジェクトを上書きし得る。今回は対応せず、今後の対応として課題化(S3 アップロードURLの key 無検証)。
現状維持と判断した点
image/jpeg 化(画像サイズ安定のため。透過は非対応で許容)。await putFileResponse.status の不要な await は除去済み。apps/user/actions/s3/get-upload-url.ts
action() ラッパーで認証+presigned URL 取得を集約。引数 { key, image: { mimeType } }。
apps/user/components/need-annotation-in-image-switch/_actions/get-upload-url.ts
共通化に伴い削除。参照を @/actions/s3/get-upload-url へ差し替え。
apps/user/lib/aws/s3/get-upload-url.ts
引数を mimeType → image: { mimeType } に統一。expiresIn を 60*60 → 60*5(5分)へ。
apps/user/actions/solid-floor-plan/create-solid-floor-plan.ts
入力を inputImage(base64)→ inputImageUri(文字列)へ。アップロード・MD5 計算を除去し、レコード作成のみを担当。
.../new/_components/solid-floor-plan-create-form/index.tsx
直接アップロード処理を追加(presigned URL → base64 を Blob 化して PUT → CloudFront URL 組み立て)。awsCloudFrontEndpoint を props で受領。コンポーネント名を SolidFloorPlanCreateForm へ改名。
.../new/page.tsx
getAwsCloudfrontEndpoint() をサーバー側で解決し、フォームへ渡す。
apps/user/components/photo-field/index.tsx
browser-image-compression で圧縮(JPEG・最大1MB・長辺1024)。onImageChange を Promise<void> | void 許容に。カメラ撮影経路を削除。
apps/user/components/need-annotation-in-image-switch/index.tsx
共通 getUploadUrl を参照し、引数を image: { mimeType } 形へ更新。
apps/system-manager/lib/aws/s3/get-upload-url.ts
エラーログに key / mimeType を追加(観測性向上のみ)。
apps/user/package.json
browser-image-compression 2.0.2 を追加。
新規作成で画像をアップロード後、CloudFront/S3 の実体を aws s3 cp s3://.../solid-floor-plans/<md5>.jpg - | head -c 32 | xxd で確認。
修正後は先頭が ff d8 ff e0 ... 4a46 4946(JFIF) となり、正常な JPEG バイナリが保存されることを確認した。