[Next.js]Cloudinaryでasset_folderを動的に設定してみた

## はじめに
Next.jsにチャレンジしていく過程をメモ/備忘録として記録していきます。
同じようにこれから始める方の参考となればと思います。

## やりたいこと
CloudinaryのNode.js SDKを使って、ディレクトリ（asset_folder）をユーザー毎に設定してアップロードしたい

## きっかけ
前回```next-cloudinary```というライブラリを使って簡単に、widgetを使ってのアップロードを実装してみました。
widgetを使い簡単かつ便利に使えそうでした。

https://qiita.com/mkthrkw/items/0f665ebba94c8024dda1

ただ、widgetがcloudinary感丸出しなのと、アプリ上でユーザー側からアップロードする場合、フォルダー（asset_folder）がユーザーID毎に分かれていた方が管理しやすそうだなと思いました。
uploadPresetで設定する方法以外を見つけきれなかったので、バックエンドSDKを使ってやってみます。

## 環境
下記のDocker開発環境にて行います。

https://qiita.com/mkthrkw/items/30115c9ac54c2204faef



## Node.js SDKのアップロードについて

Node.js SDK は、CloudinaryのアップロードAPI をラップしており、より簡単にアップロードできるようになっていました。

https://cloudinary.com/documentation/node_image_and_video_upload

基本的にアップロードは下記だけです。
```typescript
function upload(file, options).then(callback)
```

そこに設定情報も加え実際に使用する形にしていくと

### 1.まずは```.env```に環境変数を設定

```typescript:.env
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="<Your Cloud Name>"
NEXT_PUBLIC_CLOUDINARY_API_KEY="<Your API Key>"
CLOUDINARY_API_SECRET="<Your API Secret>"
```

### 2.upload用関数を作成
```cloudinary.config```に```.env```に設定した環境変数を読み込み、
それを使って```cloudinary.uploader.upload```を実行
アップロード先のフォルダをオプションにて設定できます。
その他にも多数のオプションがあります。

https://cloudinary.com/documentation/image_upload_api_reference#upload_optional_parameters

```typescript
"use server";

import { v2 as cloudinary } from "cloudinary";

cloudinary.config({
  cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
  api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

export async function uploadImage(fileData:string, asset_folder:string) {
  const options = {
    asset_folder: asset_folder,
  }
  const result = await cloudinary.uploader.upload(fileData,options);
  console.log(result);
}
```

こちらは下記のようなレスポンスが返ってきます。

```typescript
{ 
  public_id: 'cr4mxeqx5zb8rlakpfkg',
  version: 1571218330,
  signature: '63bfbca643baa9c86b7d2921d776628ac83a1b6e',
  width: 864,
  height: 576,
  format: 'jpg',
  resource_type: 'image',
  created_at: '2017-06-26T19:46:03Z',
  bytes: 120253,
  type: 'upload',
  url: 'http://res.cloudinary.com/demo/image/upload/v1571218330/cr4mxeqx5zb8rlakpfkg.jpg',
  secure_url: 'https://res.cloudinary.com/demo/image/upload/v1571218330/cr4mxeqx5zb8rlakpfkg.jpg' 
}
```


## アプリへ導入

### 1.ファイル選択用のinputを設置

・ファイルダイアログで選択出来るように```<input type="file" />```を設置します。
・そのままだと、「ファイルを選択〜」という形になってしまうので、inputはhidden属性とします。
・その代わりのボタンを配置し、onClickの処理でinputをクリックするようにしました。

```typescript
const inputRef = useRef<HTMLInputElement>(null);
```
```typescript
  <div className='relative left-10 bottom-8'>
    <div
      onClick={() => inputRef.current?.click()}
      className="btn w-11 h-11 rounded-full bg-slate-700/70 hover:bg-slate-500/70"
    >
      <CameraIcon width={24} height={24} addClass='fill-slate-400'/>
    </div>
    <input
      type="file"
      className='hidden'
      ref={inputRef}
      onChange={handleChange}
    />
  </div>
```

### 2.base64へエンコードして関数実行
・cloudinaryへアップロードする時のファイルソースがBase64に対応しているため、変換してアップロードします。


https://cloudinary.com/documentation/upload_parameters#file_sources


```typescript
  const handleChange = async () => {
    const file = inputRef.current?.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = () => {
      if(!reader.result){
        console.log('ファイルの読み込みに失敗しました。');
        return;
      }
      uploadAvatar(reader.result as string, projectDetail.id);
      router.refresh();
    };
    reader.readAsDataURL(file);
  }
```

Cloudinaryへアップロードした後、
```secure_url```をレコードの```image_url```へ登録

```typescript
function　uploadAvatar（fileData: string, projectId: string）{
    // Cloudinaryにアップロード
    const results = await uploadImage(fileData, projectId);
    // データベースに登録
    // Some action ・・・
    // results.secure_urlをimage_urlへ登録
}
```

Cloudinaryの登録時は、
・asset_folderをユーザー毎に
・public_id（オブジェクト名）はプロジェクトID
としました。
容量節約のため、画像を変更すると毎回上書きされるようにしました。

```typescript
"use server";

import { getUser } from "@/features/user/actions";
import { v2 as cloudinary } from "cloudinary";

cloudinary.config({
  cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
  api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

export async function uploadImage(fileData:string, public_id:string = '') {
  const user = await getUser(); // ユーザー情報を取得
  const options = {
    asset_folder: 'tms-app/' + user.id,
    public_id: public_id,
  }
  return await cloudinary.uploader.upload(fileData,options);
}
```

### 3.画像を更新して表示

当初はDB側にはpublic_idのみを持たせてようと考えていましたが、
毎回同じ名前で上書きされるようにしたいので、そのままだとブラウザのキャッシュにより画像が更新されませんでした。

Cloudinaryの```secure_url```はアップロードする度に変わるv〇〇〇〇〇の番号が入っているので
そのまま格納することにしました。

```typescript
import defaultImg from '@/public/images/project/default.jpeg';
```

イメージ登録前は、publicディレクトリに置いた画像を表示しています。
useEffectを使って、DBにURLが登録されると表示画像も更新されるようにしました。

```typescript
  const [imageSrc, setImageSrc] = useState<string | undefined>(defaultImg.src);

  useEffect(() => {
    if(projectDetail.image_url){
      setImageSrc(projectDetail.image_url);
    }
  }, [projectDetail.image_url]);
```

urlで格納しているため画像表示も```CldImage```は使わずimgタグを使いました。

```typescript
  <div className="avatar">
    <div className="w-32 rounded-full">
      <img
        src={imageSrc}
        width="150"
        height="150"
        alt="avatar"
      />
    </div>
  </div>
```




## テスト
無事、下記の流れで画像登録＋表示更新されることが確認できました。
1.画像変更（カメラ）ボタンを押す
2.ダイアログが開くのでファイルを選ぶ
3.Cloudinaryにアップロード　＋　DBへURL登録され、しばらくすると画像が自動更新される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680973/cfcae4ec-f287-a8ac-976e-92780518cd93.png)


## さいごに

Widgetを使わずバックエンドSDKでアップロードする方が、実際には使うケースが多そうです。
無料で十分使えてこれだけ簡単に組み込めるので、Cloudinaryを今後も使っていこうと思います。

## 参考

https://cloudinary.com/documentation/node_image_and_video_upload

https://cloudinary.com/documentation/image_upload_api_reference#upload_optional_parameters

https://cloudinary.com/documentation/upload_parameters#file_sources