Nuxt(Vuetify) + Rails 이미지 파일 업로드 하기

Nuxt.js·2021년 9월 5일
0

시행착오

목록 보기
2/6

Nuxt.js(Vuetify) + Rails 이미지 파일 업로드 하기

vue.js 단 form 구성
폼을 하나 만들고 v-file-input을 하나 배치 한다. @change이벤트를 잡는다.

<template>

  <v-form v-if="User" @submit.prevent="updateUser">
    <v-row>
      <v-col cols="12" sm="6">
        <v-img
          :src="photoPreviewUrl"
          :loading="uploading"
          min-height="180"
          class="pink lighten-4 rounded border"
        />
      </v-col>
      <v-col cols="12" sm="6">
        <v-file-input
          v-model="selectedFile"
          show-size
          outlined
          truncate-length="15"
          prepend-icon="mdi-camera"
          label="프로필사진"
          placeholder="PNG, GIF, JPG"
          accept="image/png, image/gif, image/jpeg"
          :loading="uploading"
          @change="onFileChange"
        >
          <template #selection="{ text }">
            <v-chip small label color="primary">
              {{ text }}
            </v-chip>
          </template>
        </v-file-input>
      </v-col>
    </v-row>
        <v-btn
          color="red"
          outlined
          aria-label="사진 삭제"
          @click.stop="deletePhoto"
        >
          사진 삭제
        </v-btn>
      </v-col>
    </v-row>
  </v-form>
</template>



<script>
export default {
  name: 'PhotoEdit',
  data: () => ({
    sending: false, // 폼 제출시
    selectedFile: null,
    selectedFileLocalUrl: null,
    photoOri: null,
  }),
  computed: {
    // 페이지에 보여주는 썸네일의 url을 리턴한다
    photoPreviewUrl() {
      // 만약 임시로 업로드한 로컬파일이 있다면 그 url을 우선 리턴한다
      if (this.selectedFileLocalUrl) {
        return this.selectedFileLocalUrl
      }
      return null
    },
  },
  mounted() {
    this.getMyPhoto()
  },
  methods: {
    getMyPhoto() {
      this.$store
        .dispatch('user/user_show_myphoto')
        .then((resp) => {
          const photo = resp.data.content.photo
          this.photoOri = photo
        })
        .catch((err) => {
          alert(err)
        })
        .finally(() => {
          this.sending = false
        })
    },

    async updateUserPhoto() {
      this.uploading = true
      await this.$store
        .dispatch('user/user_update_photo', this.selectedFile)
        .then((resp) => {
          const photo = resp.data.content.photo
          this.photoOri = photo
        })
        .catch((err) => {
          alert(err)
        })
        .finally(() => {
          this.uploading = false
        })
    },

    updateUser() {
      this.updateUserPhoto()
    },

    onFileChange(payload) {
      this.selectedFile = payload
      if (payload) {
        // 임시로 로컬에 선택한 이미지 파일 url을 생성한다
        this.selectedFileLocalUrl = URL.createObjectURL(payload)
      } else {
        // 파일 선택 취소 한 경우
        this.photoOri = null
      }
    },

    deletePhotoReal() {
      if (this.selectedFileLocalUrl) {
        // 새 파일 있는 경우 삭제
        this.selectedFileLocalUrl = null
      } else {
        // 기존 파일 삭제
        this.photoOri = false // false 면 변경이 있음(강제삭제)

        this.uploading = true
        // 삭제 API 호출
        this.$store
          .dispatch('user/user_delete_photo')
          .then((resp) => {
            this.photoNew = null
            alert('保存しました')
          })
          .catch((err) => {
            alert(err)
          })
          .finally(() => {
            this.uploading = false
          })
      }
    },

    async deletePhoto() {
      this.deletePhotoReal()
    },
  },
}
</script>

스토어 구성
실질적인 API연동 부분이다. CRUD가 되어야 한다.
store/user/actions.js

...
  user_show_myphoto({ commit }, payload) {
    return new Promise((resolve, reject) => {
      securedAxiosInstance
        .get('/api/v1/users/user_show_myphoto', {
          params: {},
        })
        .then((resp) => {
          const obj = resp.data.content.photo
          commit('set_photo', obj)
          resolve(resp)
        })
        .catch((err) => {
          reject(msg)
        })
    })
  },
    
  user_update_photo({ commit }, photo) {
    const formData = new FormData()
    formData.append('file', photo)
    return new Promise((resolve, reject) => {
      securedAxiosInstance
        .patch('/api/v1/users/user_update_photo', formData, {
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        })
        .then((resp) => {
          const photo = resp.data.content.photo
          commit('set_photo', photo)
          resolve(resp)
        })
        .catch((err) => {
          reject(msg)
        })
    })
  },

  user_delete_photo({ commit }) {
    return new Promise((resolve, reject) => {
      securedAxiosInstance
        .delete('/api/v1/users/user_delete_photo')
        .then((resp) => {
          commit('set_photo', {})
          resolve(resp)
        })
        .catch((err) => {
          reject(msg)
        })
    })
  },
...

스토어 세터이다.
store/user/mutations.js

...
  set_photo(state, photo) {
    state.photo = photo
  },
...

스토어 메인파일이다.
store/user/index.js

import actions from './actions'
import getters from './getters'
import mutations from './mutations'

export const state = () => ({
  photo: null,
})

export default {
  namespaced: true,
  state,
  actions,
  mutations,
  getters,
}

스토어 게터이다.
store/user/getters.js

export default {
  photo: (state) => state.photo,
}

루비온레일즈 : 백엔드 쪽 레퍼런스 설명

잼파일 설치
Gemfile 파일에 아래 라인 추가. 이미지 업로드 라이브러리는 carrierwave이다.

...
gem 'carrierwave'
...

bundle install 실행

API용 라우터 추가.
routes.rb

...
      get 'users/user_show_myphoto'
      patch 'users/user_update_photo'
      delete 'users/user_delete_photo'
...

유저 컨트롤러 구현
users_controller.rb

...
  # GET
  def user_show_myphoto
    user = User.find params[:id]
    if user
      content = {
        photo: user.thumbnails
      }
      return render_200 content
    else
      return render_401
    end
    render_404
  end
  # PATCH
  def user_update_photo
    photo = params[:file]
    if @user
      user = User.find @user.id
      if photo.present?
        if user.push_photo photo
          photo = user.thumbnails
          content = {
            photo: photo # 방금 업데이트 한 사진을 리턴 해야 한다.
          }
          return render_200 content
        else
          return render_422(user.errors)
        end
      end
    else
      return render_401
    end
    render_422
  end
  # DELETE
  '''
  프로필 사진을 삭제 한다.
  '''
  def user_delete_photo
    if @user
      if @user.delete_photo
        return render_200
      end
    end
    render_422
  end
...

유저 모델 user.rb 구현 : uploader 마운트를 잘 해준다.

...
  mount_uploaders :avatars, AvatarUploader
  serialize :avatars, JSON
...
  def thumbnails
    photo = self.last_photo
  end
  
  def push_photo photo
    user = self
    user.photos += [ photo ]
    user.save
  end
  
  def delete_photo
    user = self
    user.avatars = []
    user.save
  end
...

마이그레이션 만들기
rails g migration add_avatars_to_users avatars:string
rake db:migrate로 마이그레이트 실행

uploaders/avatar_uploader.rb

class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  # 썸네일 만들기
  version :thumb_xs  do
    process resize_to_fill: [8, 8]
  end
  version :thumb_sm  do
    process resize_to_fill: [64, 64]
  end
  def extension_allowlist
    %w(jpg jpeg gif png bmp)
  end
end

이미지 썸네일 제작 라이브러리는 MiniMagick을 사용 했다.

profile
Nuxt.js

0개의 댓글