0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsでPublic配下に画像をアップロードする記事が少なかったので、備忘録として残しておきます。

画像のアップローダの作り方は

【React/TypeScript】単一画像をReact Hook Formを使って送信する

の上記記事を参考にしてつくりました。(画像アップローダーの詳しい説明は上記記事に詳しく解説されており、非常にわかりやすいです)

ここでは、Next.js配下のPublic配下のimagesフォルダに画像を保存するまでの流れを記載していきます。

事前準備

  • formidable-serverlessのモジュールの追加
npm i formidable-serverless

フロントエンドの処理

ざっと、フロントエンドの処理です。

inputにref属性を加え、画像に変更があった場合、新しい画像を取得するコンポーネントです。

components/InputImage.tsx
import React, { InputHTMLAttributes, forwardRef } from 'react'

export type Props = {
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  id: InputHTMLAttributes<HTMLInputElement>['id']
}

const InputImage = forwardRef<HTMLInputElement, Props>(({ onChange, id }, ref) => {
  return <input ref={ref} id={id} type='file' accept='image/*' onChange={onChange} hidden />
})

InputImage.displayName = 'InputImage'

export default InputImage

プレビュー用の画像処理

hook/useGetImageUrl.ts
import { useEffect, useState } from 'react'

type Args = {
  file: File | null
}

export const useGetImageUrl = ({ file }: Args) => {
  const [imageUrl, setImageUrl] = useState('')

  useEffect(() => {
    if (!file) {
      return
    }

    let reader: FileReader | null = new FileReader()
    reader.onloadend = () => {
      // base64のimageUrlを生成する。
      const base64 = reader && reader.result
      if (base64 && typeof base64 === 'string') {
        setImageUrl(base64)
      }
    }
    reader.readAsDataURL(file)

    return () => {
      reader = null
    }
  }, [file])

  return {
    imageUrl,
  }
}

メインのフロントエンドの処理は、画像の処理のみ書いてます。tailwind.cssを使っています。

pages/posts/index.tsx
import Image from 'next/image';
import React, { FC, useRef, useState } from 'react';
import InputImage from '@/components/InputImage';
import { useGetImageUrl } from '@/hook/useGetImageUrl';

// 画像のID,ファイルのサイズ
const IMAGE_ID = 'imageId';
const FIELD_SIZE = 210;

const Post: FC = () => {
	const fileInputRef = useRef<HTMLInputElement>(null);
	const [imageFile, setImageFile] = useState<File | null>(null);
	// 画像のURLを取得
	const { imageUrl } = useGetImageUrl({ file: imageFile });

	// ファイルの設定
	const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		if (e.currentTarget?.files && e.currentTarget.files[0]) {
			const targetFile = e.currentTarget.files[0];
			setImageFile(targetFile);
		}
	};

	// 画像のキャンセルの設定
	const handleClickCancelButton = (
		e: React.MouseEvent<HTMLButtonElement, MouseEvent>
	) => {
		e.preventDefault();
		setImageFile(null);
		// <input />タグの値をリセット
		if (fileInputRef.current) {
			fileInputRef.current.value = '';
		}
	};

	const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		if (!imageFile) {
			alert('画像を選択してください');
			return;
		}
		try {
			const formData = new FormData();
			const file = imageFile;
			const blob = file.slice(0, file.size, file.type);
			// ファイル名をユニークにする
			const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
			const newFile = new File([blob], uniqueSuffix + '-' + file.name, {
				type: file.type,
			});
			formData.append('file', newFile);
			const response = await fetch('/api/images/upload', {
				method: 'POST',
				body: formData,
			});
			if (response.status === 200) {
				console.log('画像がアップロードされました');
				/*
            newFile.name:画像名
            成功した時にDBに画像名を登録する処理を追加したい場合その処理を記述
            */
			} else {
				console.log('アップロードに失敗しました');
				throw new Error('アップロードに失敗しました');
			}
			alert('登録が完了しました');
		} catch (error) {
			console.error('画像のファイル登録が失敗しました。', error);
		}
		return (
			<div>
				<h2>画像アップローダー</h2>
				<form onSubmit={handleSubmit}>
					<div className="flex rounded-lg bg-white shadow-sm my-4">
						<span className="px-4 py-2 inline-flex items-center min-w-fit rounded-s-md border border-e-0 border-gray-200 bg-gray-600 text-sm text-gray-200 ">
							画像をアップロードする
						</span>
						<label
							htmlFor={IMAGE_ID}
							className={
								'border-white-3px-dotted w-[' +
								FIELD_SIZE +
								'px] h-[' +
								FIELD_SIZE +
								'px] flex justify-center items-center overflow-hidden cursor-pointer bg-sky-200'
							}
						>
							{imageUrl && imageFile ? (
								<Image
									src={imageUrl}
									alt="アップロード画像"
									width={FIELD_SIZE}
									height={FIELD_SIZE}
									className="object-cover w-full h-full"
								/>
							) : (
								'+ 画像をアップロード'
							)}
							{/* ダミーインプット: 見えない */}
							<InputImage
								ref={fileInputRef}
								id={IMAGE_ID}
								onChange={handleFileChange}
							/>
						</label>

						<div style={{ height: 20 }} />
						{/* キャンセルボタン */}
						<button
							className="font-bold bg-gray-200 p-4"
							onClick={(e) => handleClickCancelButton(e)}
						>
							キャンセル
						</button>
					</div>

					<button
						type="submit"
						className="w-full sm:w-auto py-3 px-4 inline-flex justify-center items-center gap-x-2 text-md font-semibold rounded-lg border border-transparent bg-gradient-to-r from-purple-600 to-blue-400 hover:opacity-80 text-white disabled:opacity-50 disabled:pointer-events-none"
					>
						画像をアップロードする
					</button>
				</form>
			</div>
		);
	};
};

と、参考記事を例にすると、このように、フォームを作成することができます。

APIの作成

それでは、ここから、Public配下に画像を設置するようにapiを作っていきます。

pages/api/images/upload.ts
import fs from 'fs'
import path from 'path'
import formidable, { File, IncomingForm } from 'formidable-serverless'
import { NextApiRequest, NextApiResponse } from 'next'

// ファイルのアップロード処理
export const config = {
  api: {
    bodyParser: false,
  },
}

export default async function handler(req: NextApiRequest | File | Date, res: NextApiResponse) {
  const form = new IncomingForm()
  form.uploadDir = './public/images'
  form.keepExtensions = true
  form.parse(req, (err, fields, files: { [key: string]: any }) => {
    if (err) {
      res.status(500).json({ error: 'ファイルのアップロード中にエラーが発生しました' })
      return
    }

    const file = files.file
    if (!file) {
      res.status(400).json({ error: 'ファイルが見つかりません' })
      return
    }

    // アップロードされたファイルをpublic/imagesに移動
    const newPath = path.join(process.cwd(), 'public', file.name)
    fs.rename(file.path, newPath, (err) => {
      if (err) {
        res.status(500).json({ error: 'ファイルの保存中にエラーが発生しました' })
        return
      }
      res.status(200).json({ message: 'ファイルが正常にアップロードされました' })
      return
    })
  })
}

ただこのままだと、型エラーが出ます。

それを消していきたいと思います。

プロジェクト配下か、typesのディレクトリに、formidable-serverless.d.tsを設置します。

formidable-serverless.d.ts
declare module 'formidable-serverless' {
  import { IncomingForm as OriginalIncomingForm, File } from 'formidable'

  export interface IncomingForm extends OriginalIncomingForm {
    uploadDir: string
    keepExtensions: boolean
    parse(req: any, callback: (err: any, fields: any, files: { [key: string]: File }) => void): void
  }

  export interface File {
    size: number
    path: string
    name: string
    type: string
    hash: string
    lastModifiedDate?: Date
    originalFilename: string
    toJSON: () => string
  }

  // formidableのIncomingFormを拡張
  export class IncomingForm extends OriginalIncomingForm {
    parse(
      req: any,
      callback: (err: any, fields: any, timestamp: any, files: { [key: string]: File }) => void,
    ): void
  }
}

型は便宜上anyにしている個所がありますが、厳密に指定した場合は、適宜変更すると良いと思います。

参考になりましたら幸いです。

参考サイト

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?