ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이미지 다루기 with 미디어 라이브러리(Media Library) + S3
    Laravel 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

    댓글

Designed by Tistory.