프론트엔드/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