ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 여러 파일 | 이미지 input 만들기 (여러 이미지)
    프론트엔드/Vue 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

    댓글

Designed by Tistory.