Nuxt3とCloudinaryを使ったアップロードと削除の処理を実装する

Nuxt3経由でSaaSの`Cloudinary`を操作して画像のアップロードと削除を実装してみます。

# 完成イメージ
## 画像のアップロード
左端のアップロードボタンをクリックして、エクスプローラを起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/a1855723-797b-48e6-83cf-4b47e964a769.png)

エクスプローラから、アップロードしたい画像を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/6ca03b82-2697-490b-80a4-fcc33da94002.png)

画像のアップロードが正常に完了すると、ブラウザに画像が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/e9afcfa6-c2df-40fc-ade7-0d9cb47313b4.png)

Cloudinaryのダッシュボードを見ると、アップロードされた画像が表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/33093b06-c93a-4119-801a-202787e7ab2d.png)



## 画像の削除
つづいて、画像を削除する方法です。`Delete`ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/421eb947-3bb1-4017-89e7-d4eb39450f8c.png)

削除が正常に完了すると、メッセージが表示されます。メッセージダイアログが消えると、ブラウザからも画像が削除されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/6340b584-9efa-4dc7-a293-717a51dedf6e.png)

Cloudinaryのダッシュボードを見ると、アップロードされた画像が消えています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/c33d8594-c883-483f-a48a-4784864d38ad.png)

# ディレクトリ構造

```
.
└── NuxtDjangoProject/
    ├── backend/
    │   ├── adminuser
    │   ├── api/
    │   │   ├── migrations/
    │   │   │   └── 0001_initial.py
    │   │   ├── serializer.py
    │   │   ├── urls.py
    │   │   └── views.py
    │   └── todoproject/
    │       ├── settings.py
    │       └── urls.py
    └── frontend/
        ├── components/
        │   ├── footer/
        │   │   └── footer.vue
        │   └── navbar/
        │       └── navbar.vue
        ├── pages/
        │   ├── adminUsers/
        │   │   ├── login/
        │   │   │   └── login.vue
        │   │   ├── registration/
        │   │   │   └── registration.vue
        │   │   ├── updateProfile/
        │   │   │   └── updateProfile.vue
        │   │   ├── uploadImageFile/
        │   │   │   └── uploadImageFile.vue
        │   │   └── cloudinaryImageFile/
        │   │       └── cloudinaryImageFile.vue
        │   ├── login/
        │   │   └── login.vue
        │   ├── registration/
        │   │   └── registration.vue
        │   └── index.vue
        ├── .env
        ├── server/
        │   ├── api/
        │   │   ├── uploadToCloudinary.ts
        │   │   └── deleteCloudinary.ts
        │   └── uploadthing.ts
        ├── app.vue
        ├── nuxtconfig.ts/
        │   └── package.json
        └── tsxconfig.json
```


## ✅ 前提条件
Cloudinary アカウントを持っていること。

Nuxt 3 プロジェクトが作成済みであること。

Cloudinary の API キー、シークレット、クラウドネームを取得済み。

## 🔧 ① Cloudinary へ画像をアップロードする
### ◾ 方法の概要
Nuxt3 では、クライアント側で直接 Cloudinary にアップロードするか、サーバー経由で行う方法があります。

セキュリティ面を考慮して、**サーバー経由（API routes）**でアップロードする方法を推奨します。

## 📦 必要パッケージのインストール
```
npm install cloudinary
```

https://www.npmjs.com/package/cloudinary

#### 上記↑のパッケージをインストールするだけで`Cloudinary`は使えるようになります。




## 📁 server/api/uploadToCloudinary.ts の作成（サーバー側）
```frontend/server/api/uploadToCloudinary.ts
// server/api/upload.ts
import { v2 as cloudinary } from 'cloudinary'
import { defineEventHandler, readBody } from 'h3'

// Cloudinary 設定
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  const { file } = body // Base64 or URL-encoded string

  try {
    const result = await cloudinary.uploader.upload(file, {
      folder: 'nuxt-uploads' // 任意のフォルダ名
    })
    return { url: result.secure_url, public_id: result.public_id }
  } catch (e) {
    return { error: 'Upload failed', details: e }
  }
})

```

## ✅ クライアントからアップロードする例（Base64 画像）
```frontend/pages/cloudinaryImageFile/cloudinaryImageFile.vue
<script setup>
const uploadImage = async (file) => {
  const base64 = await toBase64(file)
  const res = await $fetch('/api/upload', {
    method: 'POST',
    body: { file: base64 }
  })
  console.log('Upload result:', res)
}

const toBase64 = (file) =>
  new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = () => resolve(reader.result)
    reader.onerror = reject
  })
</script>

<template>
  <input type="file" @change="e => uploadImage(e.target.files[0])" />
</template>

```

# 🗑️ ② Cloudinary の画像を削除する
## 📁 `server/api/deleteCloudinary.ts `の作成（サーバー側）
```frontend/server/api/deleteCloudinary.ts
// server/api/delete.ts
import { v2 as cloudinary } from 'cloudinary'
import { defineEventHandler, readBody } from 'h3'

// Cloudinary 設定
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  const { public_id } = body

  try {
    const result = await cloudinary.uploader.destroy(public_id)
    return { result }
  } catch (e) {
    return { error: 'Delete failed', details: e }
  }
})

```

## ✅ クライアントから削除する
修正ポイントは下記の図のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/cb189663-cb51-4769-bee8-6a2da8ebc359.png)


```frontend/pages/cloudinaryImageFile/cloudinaryImageFile.vue
<template>
    <v-app>
        <input type="file" @change="e=>uploadImage(e.target.files[0])" />
        <div v-if="imageUrl">
            <img :src="imageUrl" alt="imageUrl"/>
            <v-btn
                type="button"
                color="primary"
                @click="clickDeleteImage"
            >
                Delete
            </v-btn>
        </div>
    </v-app>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const imageUrl = ref<string>('');
const publicId = ref<string>('');

const uploadImage = async(file)=>{
    const base64 = await toBase64(file);
    const res = await $fetch('/api/uploadToCloudinary',{
        method:'POST',
        body:{file:base64}
    });
    console.log('Upload result:',res);
    publicId.value = res.public_id;
    imageUrl.value = res.url;
}

const toBase64 =(file)=>
    new Promise((resolve,reject)=>{
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onloadend = ()=>resolve(reader.result);
        reader.onerror = reject
    })
const clickDeleteImage = async()=>{
    if (!publicId.value) return

    const res = await $fetch('/api/deleteCloudinary',{
        method:'POST',
        body:{public_id:publicId.value}
    });
    alert('Success to delete.');
    console.log('Delete res is:',res);
    publicId.value = '';
    imageUrl.value = '';

}
</script>

```

# 🔐 環境変数設定 `.env`
```.env
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
```

`.env `を読み込ませるには` nuxt.config.ts `に次を追加：
```nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    cloudinaryCloudName: process.env.CLOUDINARY_CLOUD_NAME,
    cloudinaryApiKey: process.env.CLOUDINARY_API_KEY,
    cloudinaryApiSecret: process.env.CLOUDINARY_API_SECRET,
  }
})

```

サーバーAPIの中で` useRuntimeConfig() `で呼び出す方法に変更することもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/17099f64-cf87-4b89-87ed-4a1be464348b.png)

# 別の使用方法Nuxt3の公式サイトにあるCloudinaryの使い方について

https://cloudinary.nuxtjs.org/getting-started

上記のサイトを使って作成したい人はこちら↑をどうぞ。

#### 今回の記事では、下記のCloudinary SDK（cloudinaryパッケージ）を直接使っています。

https://www.npmjs.com/package/cloudinary

#### したがってモジュール` @nuxtjs/cloudinary `は不要です。
#### そのため、`nuxt.config.ts`にも`Cloudinary`のモジュールは追加していません。

```nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2024-11-01',
  devtools: { enabled: true },
  modules: ['vuetify-nuxt-module', '@pinia/nuxt','@uploadthing/nuxt'],
  pinia:{
    authImports:[
      'defineStore'
    ],
  },
  cloudinary:{
    cloudName:'dyfcxbadq',
  },
  /*
  uploadthing:{
    routerPath:"",
  },
  */
})
```

以上です。