프론트엔드/Vue

여러 파일 | 이미지 input 만들기 (여러 이미지)

짱구를왜말려? 2022. 8. 31. 13:52
반응형
SMALL

# What?

파일을 여러개 업로드할 때 쓰는 input

 

# How?

1. 백엔드 준비

- 모델에서 File Attribute [파일명, url, 파일타입] 내보내도록 설정하기

@ User.php

    // 단일파일
	public function getImgAttribute()
    {
        if($this->hasMedia('img')) {
            $media = $this->getMedia('img')[0];

            return [
                "id" => $media->id,
                "name" => $media->file_name,
                "url" => $media->getFullUrl()
            ];
        }

        return null;
    }

    // 복수파일
    public function getImgsAttribute()
    {
        $items = [];

        if($this->hasMedia('imgs')) {
            $medias = $this->getMedia('imgs');

            foreach($medias as $media){
                $items[] = [
                	"id" => $media->id,
                    "name" => $media->file_name,
                    "type" => $media->mime_type,
                    "url" => $media->getFullUrl(),
                ];
            }
        }

        return $items;
    }

 

2) 생성 / 수정 Controller

   public function store(Request $request)
    {
        ...

        if(is_array($request->file("imgs"))){
            foreach($request->file("imgs") as $file){
                $item->addMedia($file["file"])->toMediaCollection("imgs", "s3");
            }
        }

        if($request->imgs_remove_ids){
            $medias = $item->getMedia("imgs");

            foreach($medias as $media) {
                foreach($request->imgs_remove_ids as $id){
                    if((int) $media->id == (int) $id){
                        $media->delete();
                    }
                }
            }
        }

        return $this->respondSuccessfully();
    }

 

2. 프론트 준비

@ InputImgs.vue

* 이미지태그에 crossorigin="anonymous" 넣어야돼

<template>
    <div class="m-input-images type01">
        <div class="m-input" v-if="!onlyShow">
            <input type="file" :id="id" @change="changeFile" accept="image/*" :multiple="multiple">

            <label class="m-btn" :for="id">
                <i class="xi-plus"></i>

                사진 등록
            </label>
        </div>

        <div class="m-files-wrap" v-if="defaultFiles.length > 0 || files.length > 0">
            <div class="m-files">
                <div class="m-file-wrap" v-for="(file, index) in defaultFiles" :key="index">
                    <div class="m-file" :style="`background-image:url(${file.url})`">
                        <button v-if="!onlyShow" class="m-btn-remove" @click="remove(file, index)" type="button">
                            <i class="xi-trash-o"></i>
                        </button>
                    </div>
                </div>

                <div class="m-file-wrap" v-for="(file, index) in files" :key="index">
                    <div class="m-file" :style="`background-image:url(${file.url})`">
                        <button v-if="!onlyShow" class="m-btn-remove" @click="remove(file, index)" type="button">
                            <i class="xi-trash-o"></i>
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<style>
.m-input-images.type01 {
    display: flex; width:100%;
}
.m-input-images.type01 .m-input {
    margin-right:10px;
}
.m-input-images.type01 .m-input input {
    display: none;
}
.m-input-images.type01 .m-input .m-btn {
    display: flex;
    flex-flow: column;
    justify-content: center;
    gap: 8px;
    width: 184px;
    height: 184px;
    background-color: #f5f5f5;
    text-align: center;
    font-size: 16px;
    font-weight: bold;
    color: #a7a7a7;
    border: dashed 1px #a7a7a7; cursor:pointer;
}

.m-input-images.type01 .m-input .m-btn i {
    font-size:32px; color:#a7a7a7;
}
.m-input-images.type01 .m-files-wrap {

}
.m-input-images.type01 .m-files {
    display: flex; flex-wrap:wrap;
    margin:-4px;
}
.m-input-images.type01 .m-file-wrap {
    padding:4px;
}
.m-input-images.type01 .m-file {
    width:184px; height:184px;
    position:relative;
    background-size:cover; background-position: center center;
    border:1px solid #e1e1e1;
}
.m-input-images.type01 .m-file .m-btn-remove {
    width: 20px; height:20px;
    position: absolute; top:10px; right:10px;
    border-radius:5px;
    background-color:red;
    box-shadow:0px 3px 6px rgba(0,0,0,0.16);
}
.m-input-images.type01 .m-file .m-btn-remove i {
    color:#fff;
}
</style>
<script>
export default {
    props: {
        default: {
            default() {
                return []
            }
        },
        required: {
            default: true
        },
        multiple: {
            default: false
        },
        id: {
            default: "imgs"
        },
        onlyShow: {
            default: false,
        },
        max: {
            default: 10
        },
        maxWidth: {
            default: 1920
        }
    },

    data(){
        return {
            defaultFiles: this.default,
            files: [],
            remove_ids: [],
        }
    },

    methods: {
        changeFile(event) {
            let self = this;
            let readers = [];
            let images = [];

            let length = event.target.files.length;
            let countResize = 0;

            if(!this.multiple)
                this.files = [];

            if(this.max && this.max < Array.from(event.target.files).length)
                return alert(`최대 ${this.max}개의 파일만 업로드할 수 있습니다.`);

            Array.from(event.target.files).map((file, index) => {
                readers.push(new FileReader());
                images.push(new Image());

                readers[index].readAsDataURL(file);

                readers[index].onload = function (readerEvent) {
                    images[index].onload = function () {
                        let result = self.resize(images[index]);

                        self.files.push({
                            name: result.name,
                            file: result,
                            url: URL.createObjectURL(result),
                        });

                        countResize++;

                        if(length === countResize)
                            self.$emit("change", self.files);

                        return result;
                    };

                    images[index].src = readerEvent.target.result;
                };
            });

            this.$emit("change", this.files);
        },

        remove(file, index){
            // 새로 업로드된 파일 목록 중 삭제
            if(file.id) {
                this.defaultFiles.splice(index, 1);

                this.remove_ids.push(file.id);

                return this.$emit("removed", this.remove_ids);
            }

            // 기존 업로드된 파일 목록 중 삭제
            this.files.splice(index, 1);

            this.$emit("change", this.files);
        },

        resize(image){
            let width = image.width;
            let height = image.height;
            let canvas = document.createElement("canvas");

            if(image.width > this.maxWidth){
                height *= this.maxWidth / width;
                width = this.maxWidth;
            }

            canvas.width = width;
            canvas.height = height;
            canvas.getContext('2d').drawImage(image, 0, 0, width, height);

            const dataUrl = canvas.toDataURL('image/png');

            return this.dataURLtoBlob(dataUrl);
        },

        dataURLtoBlob(dataURI){
            const bytes =
                dataURI.split(',')[0].indexOf('base64') >= 0
                    ? atob(dataURI.split(',')[1])
                    : unescape(dataURI.split(',')[1]);
            const mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
            const max = bytes.length;
            const ia = new Uint8Array(max);
            for (let i = 0; i < max; i++) ia[i] = bytes.charCodeAt(i);

            return new Blob([ia], { type: mime });
        }
    },

    mounted() {
    }
}
</script>

@ Edit.vue

<input-images id="imgs" @change="data => form.imgs = data"  :default="$auth.user.data.imgs" @removed="data => form.img_remove_ids = data"/>

* 주의사항

- s3 쓸 경우 File object를 만져서 한거라 그런건지 실서버 반영 시 cors 오류 뜸. s3 cors 정책 수정 필요

(s3 권한탭)

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "x-amz-server-side-encryption",
            "x-amz-request-id",
            "x-amz-id-2"
        ],
        "MaxAgeSeconds": 3000
    }
]
LIST