-
이미지 다루기 with 미디어 라이브러리(Media Library) + S3Laravel 2020. 4. 21. 14:48반응형SMALL
# 이미지를 다루기 위해 미디어 라이브러리와 s3를 세팅
(이미지는 용량이 크기 때문에 s3같이 저렴한 저장소 이용하는걸 권장)
1. 미디어 라이브러리 세팅
composer require spatie/laravel-medialibrary
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations" php artisan migrate php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="config"
* mysql 저버전에서는 php artisan migrate했을 때 json 타입 못쓴다는 에러가 날거임.
이럴 경우 create_media_table.php의 json을 text로 바꿔주면 됨.@ medialibrary.php
-> disk_name을 s3로 바꿔주고, max_file_size를 늘려주기(default는 너무 작음)
/* * The disk on which to store added files and derived images by default. Choose * one or more of the disks you've configured in config/filesystems.php. */ 'disk_name' => env('MEDIA_DISK', 's3'), /* * The maximum file size of an item in bytes. * Adding a larger file will result in an exception. */ 'max_file_size' => 1024 * 1024 * 1024 * 20,
2. S3 세팅
1) aws 컴포저 설치
composer require league/flysystem-aws-s3-v3
* laravel8이라면 composer.lock 지우고 다음 명령어 실행
composer require --with-all-dependencies league/flysystem-aws-s3-v3 "^1.0"
2) aws에서 s3 버킷 생성
3) IAM에서 권한 설정
IAM 검색해서 들어가기 본인 계정 클릭 권한 추가 클릭 기존 정책 직접 연결 => AmazonS3FullAccess 추가 버킷 권한 변경 4) filesystems.php에 s3 visibility를 public으로 주기(그래야 다른 사람도 이미지 볼 수 있음)
@ filesystems.php
's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), "visibility" => "public" ],
5) .env에 s3에 대해 설정
AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION= AWS_BUCKET=
3. MVC(모델, 뷰, 컨트롤러) 세팅
-> 이미지 파일을 연관시킬 모델 세팅하기
@ Module.php(Model, 모델)
class Module extends Model implements HasMedia { use InteractsWithMedia; protected $fillable = ["title", "body", "html", "css", "js"]; protected $appends = ["img"]; public function getImgAttribute() { if($this->hasMedia('images')) { $media = $this->getMedia('images')[0]; return [ "name" => $media->file_name, "url" => $media->getFullUrl() ]; } return null; } }
* 왜 url만 내보내는게 아니라 file_name까지 보내?
-> 데이터 수정 시 가짜로 파일 input에 파일명이랑, 썸네일은 표시할거임(사실상 input은 null인거고), 그래서 사용자가 이미지를 수정 안했으면 null이 들어와 이미지 수정 작업을 안하는거고 수정했으면 null이 아닌 실제 파일 데이터를 받아서 이미지 수정 작업을 진행하면 돼
@ ModuleController.php(Controller, 컨트롤러)
class ModuleController extends ApiController { public function store(Request $request) { $request->validate([ "img" => "nullable|image", "title" => "required|string|max:500" ]); $module = DB::transaction(function() use($request) { $module = auth()->user()->modules()->create([ "title" => $request->title ]); $module->addMedia($request->img)->toMediaCollection("images", "s3"); return $module; }); return $this->respond($module); } public function update(Request $request) { $request->validate([ "img" => "nullable|image", "title" => "required|string|max:500" ]); $module = DB::transaction(function() use($request) { $module = auth()->user()->modules()->create([ "title" => $request->title ]); if($request->img) // 실제 이미지 수정 요청이 있을 때만 $module->addMedia($request->img)->toMediaCollection("images", "s3"); return $module; }); return $this->respond($module); } }
@ Form.jsx(View, 뷰)
import React, {useEffect, useState} from 'react'; import {setFlash} from "../../actions/commonActions"; import {connect} from "react-redux"; import InputText from "./inputs/InputText"; import InputCheckbox from './inputs/InputCheckbox'; import InputSelect from './inputs/InputSelect'; import InputTextarea from './inputs/InputTextarea'; import InputFile from './inputs/InputFile'; import InputImage from './inputs/InputImage'; import InputCodeEditor from './inputs/InputCodeEditor'; const Form = ({children, url = "", method = "", onThen = (response) => {}, onCatch = (error) => {}, defaultForm = null, setFlash}) => { let [form, setForm] = useState({ errors: {} }); let loading = false; const submit = (e) => { e.preventDefault(); if(loading) return; loading = true; let formData = new FormData(); Object.entries(form).map(item => { formData.append(item[0], item[1]); }); if(method === "patch" || method === "PATCH" || method === "put" || method === "PUT") { method = "post"; // patch, put multipart form 쓰면 데이터가 안날아가 그래서 post로 날리고 _method를 설정해주는식으로 해야돼 formData.append("_method","patch"); } axios[method](url, formData).then(response => { onThen(response.data); setFlash(response.data.message); setForm({errors: {}}); loading = false; }).catch(error => { onCatch(error.response.data); if(error.response.status === 422) return setForm({ ...form, errors: error.response.data.errors }); setFlash(error.response.data.message); loading = false; }); }; const clearError = () => { setForm({ ...form, errors: {} }) }; useEffect(() => { if(defaultForm) setForm({ ...defaultForm, errors: {} }); }, [defaultForm]); /*const mergeOnChange = (el, event) => { el.props.onChange(event); changeForm(event); };*/ return ( <form onSubmit={submit} onKeyDown={clearError}> { React.Children.map(children, el => { return el.type === "input" || el.type === "select" || el.type === "textarea" ? ( <div className="input-wrap"> {/* label */} {el.props.title ? React.createElement('p', {className: "input-title"}, el.props.title) : null} {/* input text */} {el.type === "input" && el.props.type === "text" ? <InputText form={form} setForm={setForm} el={el}/> : null} {/* input checkbox */} {el.type === "input" && el.props.type === "checkbox" ? <InputCheckbox form={form} setForm={setForm} el={el}/> : null} {/* input img */} {el.props.type === "img" ? <InputImage form={form} setForm={setForm} el={el}/> : null} {/* input file */} {el.props.type === "file" ? <InputFile form={form} setForm={setForm} el={el}/> : null} {/* textarea */} {el.type === "textarea" ? <InputTextarea form={form} setForm={setForm} el={el}/> : null} {/* select */} {el.type === "select" ? <InputSelect form={form} setForm={setForm} el={el}/> : null} {/* codeEditor */} {el.props.type === "codeEditor" ? <InputCodeEditor defaultForm={defaultForm} form={form} setForm={setForm} el={el}/> : null} {React.createElement('p', {className: "input-error"}, form.errors[el.props.name])} </div> ) : (el) }) } </form> ); }; const mapDIspatch = (dispatch) => { return { setFlash: (data) => { dispatch(setFlash(data)); } } }; export default connect(null, mapDIspatch)(Form);
@ InputImage.jsx(View, 뷰)
import React, {Fragment, useState, useEffect} from 'react'; const InputImage = ({form, setForm, el, mergeOnChange}) => { let [url, setUrl] = useState(null); let [imgChanged, setImgChanged] = useState(false); let [fakeFile, setFakeFile] = useState({ name: null, url: null }); useEffect(() => { if(form[el.props.name] && !imgChanged){ // 사용자가 파일 선택 눌러서 이미지 변경했으면, 업데이트라 해도 이미지는 새로 등록되야함. setFakeFile(form[el.props.name]); setUrl(form[el.props.name].url); setForm({ ...form, [el.props.name] : "" // 이미지를 null로 넘겨줘야 백엔드에서 그냥 수정을 안하고 넘김 }); } }, [form]); const changeForm = (event) => { setImgChanged(true); if(! event.target.files.length) return ; let file = event.target.files[0]; let reader = new FileReader(); let eventTargetName = event.target.name; reader.readAsDataURL(file); reader.onload = e => { setUrl(e.target.result); setForm({ ...form, [eventTargetName]: file }); }; }; return ( <div className={el.props.className ? el.props.className :`input-${el.props.type ? el.props.type : el.type}`}> <label htmlFor={el.props.name}>파일 선택</label> { form["img"] || fakeFile ? ( <Fragment> {/* file name */} <div className="input-file-name">{fakeFile ? fakeFile.name : form[el.props.name].name}</div> {/* file img */} <img className="input-file-img" src={url} /> </Fragment> ) : null } {React.cloneElement(el, { onChange: (event) => {el.props.onChange ? mergeOnChange(el, event) : changeForm(event); }, type: "file", id: el.props.name })} </div> ); }; export default InputImage;
LIST'Laravel' 카테고리의 다른 글
ENUM (0) 2020.05.31 엑셀(Excel) 다루기 (0) 2020.04.27 API Resource, Collection (0) 2020.03.27 라라벨 + 리액트 타입스크립트(typescript) (0) 2020.03.26 아이디 기반을 이메일에서 폰번호나 닉네임으로 바꾸는법 (0) 2020.03.26