[ckeditor4] filetools-respone-error

사용한 버전 ckeditor4.13.1

이미지를 업로드하면 파라미터값(CKEditorFuncNum 등)이 넘어오지 않고 filetools-respone-error 에러를 발생시킨다.

config.filebrowserUploadMethod: ‘form’ 옵션을 지정하면 된다.

<form action="upload.php">
    <textarea name="editor1" id="editor1" rows="10" cols="80">
            This is my textarea to be replaced with CKEditor.
    </textarea>
    <script>
        CKEDITOR.replace( 'editor1' , {
            filebrowserUploadUrl: 'upload.php',
            filebrowserUploadMethod: 'form'
        });
    </script>
</form>
#upload.php
<?php
if(isset($_FILES['upload']['name'])){
    $file = $_FILES['upload']['tmp_name'];
    $file_name = $_FILES['upload']['name'];
    $file_name_array = explode('.', $file_name);
    $extension = end($file_name_array);
    $new_image_name = rand().'.'.$extension;
    $allowed_extension = array('jpg', 'png', 'gif');
    if(in_array($extension, $allowed_extension)){
        $result = move_uploaded_file($file, 'upload/'.$new_image_name);
        $funcNum = $_GET['CKEditorFuncNum'];
        $url = 'upload/'.$new_image_name;
        $message = 'hello';
        echo "<script type='text/javascript'>window.parent.CKEDITOR.tools.callFunction($funcNum,'$url','$message');</script>";
    }
}
?>

https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-filebrowserUploadMethod

계층형 게시판 만들기

posts 테이블 구조

id아이디
subject제목
content내용
ori_id원글의 아이디값
grp_ord그룹(원글)내에서 순서
depth게시글의 깊이
  • 원글 : 최상위 글로 depth가 0이다.
  • 부모글 : 원글을 제외하고 자식을 갖는 글(depth 1이상이면서 자식을 갖는글), 첫번째 댓글(depth가 1)인 경우 부모글은 원글이 된다.
  • LAST_INSERT_ID() : 마지막으로 인서트된 id값
  • 최근에 작성한 글이 먼저 보이게 정렬한다.

게시글등록

  • 게시글을 인서트할때 반환되는 id값을 ori_id에 대입해준다.
  • grp_ord, depth는 원글이기 때문에 0을 준다.
INSERT INTO posts SET subject = '첫번째 글',
                     content = '',
                     grp_ord = 0,
                     depth = 0;
                     
UPDATE posts SET ori_id = (select last_insert_id()) WHERE id = (select last_insert_id());

댓글등록

UPDATE posts SET grp_ord = grp_order+1 WHERE ori_id = '부모글 ori_id' AND grp_ord > '부모글의 grp_ord';

INSERT INTO posts SET subject = '첫번째 글의 댓글',
                     content = '',
                     ori_id = '부모글의 ori_id',
                     grp_ord = '부모글의 grp_ord' + 1,
                     depth = '무보글의 depth' + 1;
  • 최근에 등록한 댓글이 상단에 위치하기 위해서 부모글의 grp_ord보다 큰 값을 찾아 1씩 증가시켜 순서를 뒤로 밀어준다.
  • 업데이트 문으로 숫자가 1씩 밀렸기 때문에 인서트할 때 grp_ord + 1를 줘서 댓글 순서를 처음으로 오게한다.

리스트쿼리

SELECT * FROM posts ORDER BY ori_id DESC, grp_ord

참고한 사이트

https://adgw.tistory.com/entry/계층형-게시판-알고리즘-댓글-알고리즘

[laravel] 나만보는 라라벨 튜토리얼

라라벨코리아, 라라캐스트를 보고 간단한 프로젝트를 만들면서 정리하는 글

  1. 프로젝트생성
  2. composer 의존성 설치
  3. 프론트앤드 스케폴딩
  4. npm 의존성 설치
  5. laravel-mix 사용하기
  6. model, controller, migration 생성하기
  7. restful한 route 만들기
  8. view 생성하기
  9. 기능구현하기
    • 글쓰기 폼작성
    • 저장하기
    • 글보기
    • 글 수정하기 폼작성
    • 글 업데이트하기
    • 글목록 페이지
    • 삭제하기
  10. 모델을 라우트에 바인딩 시키자
  11. 중복 제거

1.프로젝트 생성

laravel new blog
cd ./blog

2.composer 의존성 설치

composer install

3.프론트앤드 스케폴딩
필요한 부분만 선택하여 설치하면 된다. (본 예제에서는 bootstrap만 스캐폴딩함)

composer require laravel/ui --dev
// Generate basic scaffolding...
php artisan ui bootstrap
php artisan ui vue
php artisan ui react
// Generate login / registration scaffolding...
php artisan ui bootstrap --auth
php artisan ui vue --auth
php artisan ui react --auth

4.npm 의존성 설치
프론트 앤드 스캐폴딩을 하게 되면 package.json에 의존성 목록의 추가된다. 프로젝트에 사용하기 위해서 의존성을 설치해준다.

npm install

5.laravel-mix 사용하기
루트디렉토리의 webpack.mix.js 파일을 열어보면 아래와 같이 기본설정이 되어있다.

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

특별히 추가 할게 없다면 npm run dev 명령어를 실행해서 컴파일 하자. 앞에서 부트스트랩을 스캐폴딩했기 때문에 컴파일 하면 부트스트랩도 같이 컴파일 되어 public/js/app.js, public/css/app.css 로 위치하게 된다. (laravel-mix 문서 보기)

추가로 browsersyc를 webpack.mix.js에 추가해서 브라우저 변경시 새로고침없이 자동으로 리로드되게 해주면 개발할때 편리하다.

//mix.browserSync('mydomain.com');
mix.browserSync('localhost:8000');

6.모델, 컨트롤러, 마이그레이션 생성하기
-r 옵션은 리소스컨트롤러로 컨트롤러에 기본적인 메서드가 자동으로 추가된다.

* 모델과 컨트롤러는 단수로, 마이그레이션은 복수로 표현한다.
모델          Article
컨트롤러      ArticleController
마이그레이션  create_articles_table
php artisan make:model Article
php artisan make:controller ArticleController
php artisan make:migration create_aticles_table
// 한번에 생성하기 -c: controller -r:resource controller -m:migration
php artisan make:model Article -c -m
또는
php artisan make:model Article -r -m

생성된 마이그레이션 파일을 열어 테이블 스키마를 지정해준다.

    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->text('title');
            $table->longText('content');
            $table->timestamps();
            $table->boolean('is_published')->nullable()->default(false);
        });
    }
    public function down()
    {
        Schema::dropIfExists('articles');
    }
//up 메서드는 migrate 명령어가 실행될 때 호출되고 down 메서드는 migrate:rollback 될 때 호출된다.

php artisan migrate 명령어를 실행해서 테이블을 생성하면된다. 이때 한번도 마이그레이션하지 않았으면 laravel에서 제공하는 테이블까지 함께 생성된다.

7. restful한 route 만들기

//인덱스 리스트
route::get('/article/', 'ArticleController@index')->name('article.index');
//글쓰기 폼
route::get('/article/create', 'ArticleController@create')->name('article.create');
//새로운글 저장
route::post('/article', 'ArticleController@store')->name('article.store');
//해당글 보기 
route::get('/article/{id}', 'ArticleController@show')->name('article.show');
//해당글 수정 폼
route::get('/article/{id}/edit', 'ArticleController@edit')->name('article.edit');
//해당글 업데이트
route::put('/article/{id}', 'ArticleController@update')->name('article.update');
//해당글 삭제
route::delete('/article/{id}/destroy', 'ArticleController@destroy')->name('article.destroy');

route파사드의 name메서드를 사용하는 편이 나중에 url 관리하기가 편하다.

8.view 생성하기
/resources/views 디렉토리에 이번프로젝트에 뼈대가 될 layout.blade.php 파일을 생성한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="{{ asset('css/app.css')}}">
    <script src="{{ asset('js/app.js') }}"></script>
    <title>Document</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container">
            <a class="navbar-brand" href="{{ route('article.index') }}">Navbar</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">
                <li class="nav-item {{Request::is('article*')? 'active' : ''}}">
                    <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
                </li>
                </ul>
            </div>
        </div>
    </nav>
    <div class="container">
        @yield('content')
    </div>
    <div class="footer container mt-3 pt-3 text-center">
        <div class="text-muted">@laravel</div>
    </div>
</body>
</html>

/resources/views/article 디렉토리를 생성하고 앞에서 지정한 라우트에 필요한 뷰파일을 생성해준다.

//index.blade.php
@extends('layout')
@section('content')
리스트페이지
@endsection
//create.blade.php
@extends('layout')
@section('content')
글쓰기 폼
@endsection
//show.blade.php
@extends('layout')
@section('content')
글보기
@endsection
//edit.blade.php
@extends('layout')
@section('content')
글쓰기 수정 폼
@endsection
뷰파일은 /resources/views 내에 어디서든 위치해도 상관없는데 일관성을 위해서 article 디렉토리를 추가했다. 

9.기능구현하기

1)글쓰기 폼 (create.blade.php) 작성

  • ArticleController@create
    public function create()
    {
        return view('article.create');
    }
  • create.blade.php
@extends('layout')
@section('content')
    <form action="{{ route('article.store') }}" method="post">
        @csrf
        <div class="form-group">
          <label for="title">제목</label>
          <input 
          type="text"
          name="title"
          id="title"
          class="form-control @error('title') is-invalid @enderror"
          placeholder=""
          aria-describedby="title"
          value="{{ old('title') }}">
          @error('title')
          <p class="invalid-feedback">제목을 입력하세요</p>
          @enderror
        </div>
        <div class="form-group">
          <label for="content">내용</label>
          <textarea class="form-control @error('content') is-invalid  @enderror" name="content" id="content" rows="3">{{ old('content') }}</textarea>
          @error('content')
          <p class="invalid-feedback">내용을 입력하세요</p>
          @enderror
        </div>
        <button type="submit" class="btn btn-primary">저장</button>
    </form>
@endsection

form안에 @csrf 를 사용해서 csrf 토큰을 꼭 생성해야 한다.
validation처리를 위해서 @error, 글로벌 헬퍼함수 old() 부분을 잘 확인하자.
old()는 넘어온값을 세션에 저장해 두는데 validation에 통과하지 못했을 때 양식에 이전에 입력한 값을 다시 넣어주기 위해 사용한다.

2)저장하기 ArticleController@store

use App\Article; 
public function store()
    {
        request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = new Article();
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.index');
    }

3)글보기

  • ArticleController@show 작성
    public function show($id)
    {
        $article = Article::find($id);
        return view('article.show', compact('article'));
    }
  • show.blade.php 작성
@extends('layout')
@section('content')
<h1>{{ $article->title }}</h1>
<small class="small text-muted">{{ $article->updated_at }}</small>
<div class="mt-3 pt-3 border-top">{{ $article->content }}</div>
@endsection

4)글 수정하기폼 ArticleController@edit

  • ArticleController@edit
   public function edit($id)
    {   
       $article = Article::find($id); 
        return view('article.edit', compact('article'));
    }
  • edit.blade.php
@extends('layout')
@section('content')
    <form action="{{ route('article.update', $article->id) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="form-group">
          <label for="title">제목</label>
          <input 
          type="text"
          name="title"
          id="title"
          class="form-control @error('title') is-invalid @enderror"
          placeholder=""
          aria-describedby="title"
          value="{{ $erros->has('title') ? old('title') : $article->title }}">
          @error('title')
          <p class="invalid-feedback">제목을 입력하세요</p>
          @enderror
        </div>
        <div class="form-group">
          <label for="content">내용</label>
          <textarea 
          class="form-control @error('content') is-invalid  @enderror" 
          name="content" 
          id="content" 
          rows="3">{{ $erros->has('content') ? old('content') : $article->content}}</textarea>
          @error('content')
          <p class="invalid-feedback">내용을 입력하세요</p>
          @enderror
        </div>
        <button type="submit" class="btn btn-primary">저장</button>
    </form>
    <form action="{{ route('article.destroy', $article->id) }}" method="POST">
      @csrf
      @method('DELETE')
      <button type="submit" class="btn btn-danger">삭제</button>
    </form>
@endsection

@method 지시자를 이용해서 PUT메서드를 스푸핑하는것을 확인하자.

5)글 업데이트 ArticleController@update

public function update($id)
    {
        request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = Article::find($id);
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.show', $article->id);
    }

5)글목록 페이지

  • ArticleController@index
public function index(){
        $articles = Article::orderBy('created_at', 'desc')->paginate(15);
        return view('article.index', compact('articles'));
    }
  • inedx.blade.php
@extends('layout')
@section('content')
@foreach($articles as $article)
<div class="card mb-3">
  <div class="row no-gutters">
    <div class="col-md-3">
      <img src="..." class="card-img" alt="">
    </div>
    <div class="col-md-9">
      <div class="card-body">
        <h5 class="card-title">
          <a href="{{ route('article.show', $article->id) }}">{{ $article->title }}</a>
        </h5>
        <p class="card-text">{{ $article->content }}</p>
        <p class="card-text"><small class="text-muted">{{ $article->updated_at }}</small></p>
      </div>
    </div>
  </div>
</div>
@endforeach
@endsection

6)해당글 삭제하기 ArticleController@destroy

    public function destroy($id)
    {
        $article = Article::find($id);
        $article->delete();
        return redirect()->route('article.index');
    }

10. 모델을 라우트에 바인딩 시키자

라우트의 {id} 부분을 {article}로 변경하고 컨트롤러 메서드에 $id로 전달한 파라미터를 Article $article 타입 힌트로 지정한다.

만약 route의 {article}을 {foo}로 지정했다면 컨트롤러의 타입힌트도 Article $foo로 변경해야 한다.

//web.php
route::get('/article/', 'ArticleController@index')->name('article.index');
route::get('/article/create', 'ArticleController@create')->name('article.create');
route::post('/article', 'ArticleController@store')->name('article.store');
route::get('/article/{article}', 'ArticleController@show')->name('article.show');
route::get('/article/{article}/edit', 'ArticleController@edit')->name('article.edit');
route::put('/article/{article}', 'ArticleController@update')->name('article.update');
route::delete('/article/{article}/destroy', 'ArticleController@destroy')->name('article.destroy');
//ArticleController.php
    public function index(){
        $articles = Article::orderBy('created_at', 'desc')->paginate(15);
        return view('article.index', compact('articles'));
    }
    public function create()
    {
        return view('article.create');
    }
    public function store()
    {
        request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = new Article();
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.index');
    }
    public function show(Article $article)
    {
        $article = Article::find(Article $article);
        return view('article.show', compact('article'));
    }
    public function edit(Article $article)
    {   
       $article = Article::find(Article $article); 
        return view('article.edit', compact('article'));
    }
    public function update(Article $article)
    {
       request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = Article::find(Article $article);
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.show', $article->id);
    }
    public function destroy(Article $article)
    {
        $article = Article::find(Article $article);
        $article->delete();
        return redirect()->route('article.index');
    }
라우트에 모델을 바인딩하면 라라벨은 자동으로 테이블의 id칼럼을 조회하게 된다. id가 아닌 다른 칼럼을 조회하게 하려면 model에 getRouteKeyName메서드를 오버라이드 해준다.
class Article extends Model
{
  function getRouteKeyName(){
    return 'title';
  }
}

11. 중복제거

validate와 게시글 저장, 업데이트 하는부분이 store와 update에서 중복이 발생했다. validation 부분을 메서드로 작성하여 중복을 제거하자.

public function index(){
        $articles = Article::orderBy('created_at', 'desc')->paginate(15);
        return view('article.index', compact('articles'));
    }
    public function create()
    {
        return view('article.create');
    }
    public function store()
    {
        Article::create($this->validateArticle());
        return redirect()->route('article.index');
    }
    public function show(Article $article)
    {
        return view('article.show', compact('article'));
    }
    public function edit(Article $article)
    {   
        return view('article.edit', compact('article'));
    }
    public function update(Article $article)
    {
        $article->update($this->validateArticle());
        return redirect()->route('article.show', $article->id);
    }
    public function destroy(Article $article)
    {
        $article->delete();
        return redirect()->route('article.index');
    }
    protected function validateArticle(){
        return request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
    }

중복을 제거 했지만 글을 작성하거나 업데이트 하면 아래와 같이 에러가 발생할 것이다.

Add [title] to fillable property to allow mass assignment on [App\Article]. 

기본적으로 모든 Eloquent 모델은 대량 할당-Mass Assignment 으로부터 보호되기 때문에, 모델의 ‘fillable’나 ‘guarded’속성을 지정해야 주어야 한다.
fillable은 화이트리스트방식이고, guarded는 블랙리스트 방식이라고 생각하면 된다.

class Article extends Model
{
  protected $guarded = [];
  또는
  protected $fillable = ['title', 'content'];
}