[Nuxt.js] 파일 업로드 구현

babypig·2022년 10월 5일
1

Nuxt.js

목록 보기
2/10
post-thumbnail

📌 구현목표

파일첨부 기능을 개발, 10MB 이하의 PNG, JPG, JPEG, GIF, PDF 파일만 업로드할 수 있게 구현, 그리고 file이 업로드 시 input text에 file name을 출력 및 파일첨부의 기능은 다운로드 및 삭제로 대체,

⚙️ 기본구조


<div>
  <SInput class="mr-8" readonly w-size="xx-large" />
  <SButton>파일 첨부</SButton>
  <input type="file" class="is-blind"/>
</div>

//Input Components 구조 및 props data

<input
  :value="value"
  :placeholder="placeholder"
  :readonly="readonly"
  :class="{ 'is-error': isError }"
  :style="{
    minWidth: `${minWidth}rem`
  }"
  :type="type"
  :maxlength="maxlength ? maxlength : '524288'"
  @input="updateInput"
  @keyup="keyup"
  @keyup.enter="enter"
/>

<script>
export default {
  name: 'SInput',
  props: {
    value: {
      type: [String, Number],
      required: false,
      default: ''
    },
    placeholder: {
      type: String,
      required: false,
      default: ''
    },
    type: {
      type: String,
      required: false,
      default: 'text'
    },
    maxlength: {
      type: String,
      required: false,
      default: ''
    },
    readonly: {
      type: Boolean,
      required: false,
      default: false
    },
    // value: small | normal | large | x-large | xx-large | xxx-large
    wSize: {
      type: String,
      required: false,
      default: 'normal'
    },
    isError: {
      type: Boolean,
      required: false,
      default: false
    }
  },
  data() {
    return {
      minWidth: 20
    };
  },
  created() {
    let minWidth = 9.6;

    switch (this.wSize) {
      case 'small':
        minWidth = 15.2;
        break;
      case 'normal':
        minWidth = 20;
        break;
      case 'large':
        minWidth = 32;
        break;
      case 'x-large':
        minWidth = 46.0;
        break;
      case 'xx-large':
        minWidth = 58.4;
        break;
      case 'xxx-large':
        minWidth = 74.4;
        break;
      default:
        minWidth = 20;
        break;
    }

    this.minWidth = minWidth;
  },
  methods: {
    updateInput(e) {
      this.$emit('input', e.target.value);
    },
    keyup(e) {
      this.$emit('keyup', e.target.value);
    },
    enter(e) {
      this.$emit('enter', e.target.value);
    }
  }
};
</script>

1. button, input setting 및 type="file"에 연결하기

input에 accept을 이용하여 확장자를 제한하고 readonly를 통하여 읽기전용으로 만들어준 후 ref를 통하여 button 클릭 시 input file과 연결 시키기 (input은 IR기법으로 숨김처리)

<div>
  <SInput class="mr-8" readonly w-size="xx-large" />
  <SButton @click="fileClick('fileUpload')">파일 첨부</SButton>
  <input 
     ref="fileUpload"
     type="file"
     accept="image/jpg, image/jpeg, image/png, image/gif, .pdf"
     class="is-blind"
/>
</div>

<script>
    fileClick(target) {
      this.$refs[target].click();
    },
</script>

2. FormData를 활용하여 파일 받아오기

파일이 있으면 file.size를 계산하여 10MB 이하면 받아오는 api 실행 및 아닐 시 시스템 모달로 경고창 띄워주기를 작성,

<div>
  <SInput class="mr-8" readonly w-size="xx-large" />
  <SButton @click="fileClick('fileUpload')">파일 첨부</SButton>
  <input 
     ref="fileUpload"
     type="file"
     accept="image/jpg, image/jpeg, image/png, image/gif, .pdf"
     class="is-blind"
	 @change="fileChange($event, 0)"
/>
</div>

<script>
   async fileChange(e, index) {
      if (e.target.files && e.target.files[0]) {
        const file = e.target.files[0];
        const size = file.size / 1024 / 1024;

        if (size <= 10) {
          const formData = new FormData();

          formData.append('files', file);
          await this.$axios.$post('/file', formData).then((fileForm) => {
            this.detailData.files[index] = fileForm;
          });
        } else {
          this.modal.isFileError = true;
        }
        e.target.value = null;
        e.target.files = null;
      }
    },
</script>

3. Input에 파일 name 출력하기

  • 💣 문제점 발생 : 파일을 첨부하였는데 input에 파일 name이 출력되지 않아 파일을 첨부할때 this.$forceUpdate(); 를 통하여 재랜더링되게 설정.
<div>
  <SInput :value="getFileName(0)" class="mr-8" readonly w-size="xx-large" />
  <SButton @click="fileClick('fileUpload')">파일 첨부</SButton>
  <input 
     ref="fileUpload"
     type="file"
     accept="image/jpg, image/jpeg, image/png, image/gif, .pdf"
     class="is-blind"
	 @change="fileChange($event, 0)"
/>
</div>

<script>
   async fileChange(e, index) {
      if (e.target.files && e.target.files[0]) {
        const file = e.target.files[0];
        const size = file.size / 1024 / 1024;

        if (size <= 10) {
          const formData = new FormData();

          formData.append('files', file);
          await this.$axios.$post('/file', formData).then((fileForm) => {
            this.detailData.files[index] = fileForm;
          });
        } else {
          this.modal.isFileError = true;
        }
        e.target.value = null;
        e.target.files = null;
        this.$forceUpdate();
      }
    },
  getFileName(index) {
      return this.detailData.files[index]?.originalFileName || '';
    },
</script>

4. input 파일 등록 시 다운로드, 삭제 버튼 노출 및 이벤트

 <div>
            <SInput :value="getFileName(0)" class="mr-8" readonly w-size="xx-large" />
            <SButton v-if="!detailData.files[0]" @click="fileClick('fileUpload')">파일 첨부</SButton>
            <template v-else>
              <a
                :href="`/api/file/download?path=${detailData.files[0].savedFileName}&fileName=${detailData.files[0].originalFileName}`"
                download
                class="download primary mr-8"
                >다운로드</a
              >
              <SButton @click="onDeleteFile(0)">삭제</SButton>
            </template>
            <input
              ref="fileUpload"
              type="file"
              accept="image/jpg, image/jpeg, image/png, image/gif, .pdf"
              class="is-blind"
              @change="fileChange($event, 0)"
            />
          </div>

<script>
     onDeleteFile(index) {
      this.detailData.files[index] = null;
      this.$forceUpdate();
    },
</script>

💫 번외

받아온 data를 v-for로 작업 시 data값을 인자로 받아 처리

          <div>{{ getState(item.state) }}</div>

<script>
 getState(state) {
      return state === 'SCHEDULE' ? '진행예정' : state === 'PROGRESS' ? '진행' : '종료';
    }
</script>

➕ 추가

v-for로 작성시,


   <div v-for="(item, index) in 3" :key="item.index">
            <SInput :value="getFileName(index)" class="mr-8" readonly w-size="xx-large" />
            <SButton v-if="!detailData.files[index]" @click="fileClick(index)">파일 첨부</SButton>
            <template v-else>
              <a
                :href="`/api/file/download?path=${detailData.files[index].savedFileName}&fileName=${detailData.files[index].originalFileName}`"
                download
                class="download primary mr-8"
                >다운로드</a
              >
              <SButton @click="onDeleteFile(index)">삭제</SButton>
            </template>
            <input
              ref="fileUpload"
              type="file"
              accept="image/jpg, image/jpeg, image/png, image/gif, .pdf, .zip"
              class="is-blind"
              multiple
              @change="fileChange($event, index)"
            />
          </div>
          
<script>
methods: {
    async fileChange(e, index) {
      if (e.target.files && e.target.files[0]) {
        const file = e.target.files[0];
        const size = file.size / 1024 / 1024;
        if (size <= 10) {
          const formData = new FormData();
          formData.append('files', file);
          await this.$axios.$post('/file', formData).then((fileForm) => {
            this.detailData.files[index] = fileForm;
          });
        } else {
          this.modal.isFileError = true;
        }
        e.target.value = null;
        e.target.files = null;
        this.$forceUpdate();
      }
    },
    fileClick(index) {
      this.$refs.fileUpload[index].click();
    },
    getFileName(index) {
      return this.detailData.files[index]?.originalFileName || '';
    },
    onDeleteFile(index) {
      this.detailData.files[index] = null;
      this.$forceUpdate();
    }
  }
</script>

이미지 업로드 미리보기,


<script>
   thumb(event) {
      const input = event.target;
      if (input.files && input.files[0]) {
        const reader = new FileReader();
        reader.onload = (e) => {
          this.uploadImageFile = e.target.result;
        };
        reader.readAsDataURL(input.files[0]);
      }
    },
</script>
profile
babypig

0개의 댓글