【Next.js】画像投稿、更新機能ならSupabase RDB × storage

# これはなに
ここではSupabaseのRDBとstorageを使って、アップロードした画像の表示と、次回アクセス時にも画像を保持し、再びアップロードすれば更新できるようなフォームを作っていきます。
ここまで紹介しているような記事がなかなかなかったので、参考までに。

今回はNext.jsを使ったやり方を紹介します。API Routeも活用します。

# Supabaseのセットアップ
今回は工程が多いので省きます。代わりにわかりやすく紹介している記事を紹介させていただきます。`URL`と`KEY`を忘れずに保持しましょう。`NEXT_PUBLIC_`がないとクライアントでは取得できないのでつけておきます。
Nextの環境ができたらルートディレクトリにこの`.env.local`をおいてください。

```.env.local
NEXT_PUBLIC_SUPABASE_URL = https://xxxxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_KEY = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

https://zenn.dev/joo_hashi/books/a5e2247b85dc0a/viewer/51d97f

# RDB
「img-editor」というtable名で進めていきます。urlを挿入できるようにcolumnを定義しておきます。本当はあった方がよいですが今回はRLSはつけてません。

![スクリーンショット 2024-03-16 10.56.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/448df888-132d-8d59-9e62-17c4431ca8c6.png)

# Storage
「img-store」というbucket名で進めていきます。`Public`にしておいてください。フォルダの作成は特に行いませんが、こちらはポリシーの設定が必要なので行っておきます。ここもテキトーです。「Policies」から...

→「New policy」
![スクリーンショット 2024-03-16 11.01.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/fb0db279-a3e2-0013-6883-a482334fa352.png)

→「Get started quickly」
![スクリーンショット 2024-03-16 11.01.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/16ad7cde-8805-080d-d44d-538815735706.png)

「Enable read access to everyone」を選択した状態で、→「Use this template」
![スクリーンショット 2024-03-16 11.01.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/c7356c60-834d-9ef0-58be-8fddd6fccd4f.png)

以下このように設定します。→「Review」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/9cfb37cc-cf79-1c79-faf9-9da22ff5c3d5.png)

→「Save policy」
![スクリーンショット 2024-03-16 11.04.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/1b0cd570-7d68-536f-7fb6-80a74e528719.png)

とりあえずこれでAPIを叩けばすぐ使える状態になります。

# Supabaseのクライアントを作る
今回は`axios`を使ってAPIにアクセスします。今回はわかりやすいようにClassを作って、メソッドで処理しましょう。`@supabase/supabase-js`が必要なのでnpmの場合はこのようにインストールし、`import`してください。

```terminal
$ npm install @supabase/supabase-js
```

```js:ImgEditor.js
import { createClient } from "@supabase/supabase-js";

export default class ImgEditor{
    constructor(supabaseUrl, supabaseKey){
        this.supabase = createClient(
            supabaseUrl,
            supabaseKey
        );
    };

    // 画像のURLをTableから取得する
    async getImageUrl(){
        const { data, error } = await this.supabase
            .from("img-editor")
            .select("src");
        if (error) {
            throw error;
        } else {
            return data[0];
        }
    };

    // 画像をアップロードする。成功したら画像のURLを返す
    async uploadImage(fileObj){
        const { data, error } = await this.supabase.storage
            .from("img-store")
            .upload(`upload-img.${fileObj.name.split(".")[1]}`, fileObj,
                { upsert: true }
            );
        if (error) {
            throw error;
        } else {
            const src = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/${data.fullPath}`
            const { _, error } = await this.supabase
                .from("img-editor")
                .upsert({ id: 1, src: src });
            
            if (error) {
                throw error;
            } else {
                return { src: src };
            }
        }
    };
};

```
ポイントなのは`.upload(`upload-img.${fileObj.name.split(".")[1]}`, fileObj,{ upsert: true });`の部分ですが、`upload(保存先パス, fileObject, config)`という引数になっており、`{upsert: true}`にすると上書き保存が許可されます。詳しくはこちら↓

https://supabase.com/docs/guides/storage/uploads/standard-uploads

# Reactでフォームを作る
では、サンプルのフォームを作っていきます。今回は関数型コンポーネントで実装します。ハンドリングする箇所は一旦`null`にしておきます。

```jsx:page.jsx
export default function Index(){
    return (
        <>
            <input type="file" accept="image/*" onChange={null}/>
            <button onClick={null} disabled={null}>アップロード</button>
            <p>↓アップロードした画像</p>
            <img src={null} alt="selected-image" />
        </>
    )
};

```

こんな感じ
![スクリーンショット 2024-03-18 1.03.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/5f599a2d-8567-d17d-8a87-9d8f0f641c8d.png)

## 選択画像のプレビュー
次に画像を選択すると`img`に表示されるようにします。`useState`を使います。Next v13からクライアントコンポーネントには先頭に`use client`がないと動作しないのでつけ忘れに注意しましょう。

```jsx:page.jsx
'use client'

import { useState } from "react";
import ImgEditor from "@/src/ImgEditor";

export default function Page(){
    const [selectedImage, setImage] = useState();

    return (
        <>
            <input 
                type="file" 
                accept="image/*" 
                onChange={(e) => setImage(e.currentTarget.files[0])}
            />
            <button 
                onClick={null}
                disabled={
                    selectedImage == null ||
                    typeof selectedImage == "string"
                }
            >
                アップロード
            </button>
            <p>↓アップロードした画像</p>
            <img 
                src={
                    selectedImage ?
                    typeof selectedImage == "string" ? 
                    selectedImage :
                    window.URL.createObjectURL(selectedImage) :
                    null
                } 
                alt="selected-image"
            />
        </>
    )
};

```
`img`のプレビューでは`FileReader`をつかうというアプローチもありますが、`window.URL.createObjectURL`を使う方が短く済むのでおすすめです。
ただし、`null`が引数になった場合はエラーを吐くのでハンドリングします。
また、再アクセス時はアップロードされた画像のURLが入ることになるので、その場合もハンドリングしてやる必要があります。

初学者向けにまとめると、`src`の部分を`if`で書くとこうです。
```js:
if (selectedImage) {
    if (typeof selectedImage == "string") {
        return selectedImage;
    } else {
        return window.URL.createObjectURL(selectedImage);
    }
} else {
    return null;
}
```

## 画像のアップロード
アップロードされた画像の`FileObject`を`useState`で管理「アップロード」ボタンのハンドリングで、作ったクラスメソッドの`uploadImage`を呼んでみましょう。

```jsx:page.jsx
'use client'

import { useEffect, useState } from "react";
import ImgEditor from "@/src/ImgEditor";

// Supabaseクライアント
const imgEditor = new ImgEditor(
        process.env.NEXT_PUBLIC_SUPABASE_URL,
        process.env.NEXT_PUBLIC_SUPABASE_KEY    
    );

export default function Page(){
    const [selectedImage, setImage] = useState();

    // 画像アップロードのハンドリング
    const saveImage = () =>{
        imgEditor.uploadImage(selectedImage)
            .then((data) => console.log(data))
            .catch((error) => console.error(error));
    };

    return (
        <>
            <input 
                type="file" 
                accept="image/*" 
                onChange={(e) => setImage(e.currentTarget.files[0])}
            />
            <button 
                onClick={saveImage}
                disabled={
                    selectedImage == null || 
                    typeof selectedImage == "string"
                }
            >
                アップロード
            </button>
            <p>↓アップロードした画像</p>
            <img 
                src={
                    selectedImage ?
                    typeof selectedImage == "string" ? 
                    selectedImage :
                    window.URL.createObjectURL(selectedImage) :
                    null
                } 
                alt="selected-image"
            />
        </>
    )
};
```

これでアップロードされた画像はstorageおよびtableに保存されるようになったはずです。
ではテキトーな画像をアップロードしてみましょう。コンソールに`{src: ...}`の形で返ってくれば成功です。
![スクリーンショット 2024-03-18 2.57.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/dbb7eee2-9a80-594c-01a5-6c1237a75a18.png)

Supabase側で確認してもちゃんと入っているか確認しましょう。

## 画像のURLを取得する。
最後に、再アクセス時に前回アップロードした画像をテーブルから拾ってくるようにしましょう。
`useEffect`とクラスメソッドの`getImgUrl`を使いましょう。

```jsx:page.jsx
'use client'

import { useEffect, useState } from "react";
import ImgEditor from "@/src/ImgEditor";

const imgEditor = new ImgEditor(
        process.env.NEXT_PUBLIC_SUPABASE_URL,
        process.env.NEXT_PUBLIC_SUPABASE_KEY    
    );

export default function Page(){
    const [selectedImage, setImage] = useState();

    // 初回レンダリング時に画像URLを取得する
    useEffect(() => {
        imgEditor.getImageUrl()
            .then((data) => setImage(data.src))
            .catch((error) => console.error(error));
    }, []);

    const saveImage = () =>{
        imgEditor.uploadImage(selectedImage)
            .then((data) => console.log(data))
            .catch((error) => console.error(error));
    };

    return (
        <>
            <input 
                type="file" 
                accept="image/*" 
                onChange={(e) => setImage(e.currentTarget.files[0])}
            />
            <button 
                onClick={saveImage}
                disabled={
                    selectedImage == null ||
                    typeof selectedImage == "string"
                }
            >
                アップロード
            </button>
            <p>↓アップロードした画像</p>
            <img 
                src={
                    selectedImage ?
                    typeof selectedImage == "string" ? 
                    selectedImage :
                    window.URL.createObjectURL(selectedImage) :
                    null
                } 
                alt="selected-image"
            />
        </>
    )
};

```

これでページをリロードしても前回アップロードした画像が表示されたかと思います。

# おわりに
今回は画像のアップロード、プレビュー、Supabaseへの保存、更新機能まで説明させていただきました。アカウント登録時のアバターやSNSなどの画像投稿機能なんかで使えそうですね。今回はパフォーマンスやセキュリティをガン無視しましたが、実際に使う場合はそこも意識して実装してみてはいかがでしょうか。