라라벨코리아, 라라캐스트를 보고 간단한 프로젝트를 만들면서 정리하는 글
- 프로젝트생성
- composer 의존성 설치
- 프론트앤드 스케폴딩
- npm 의존성 설치
- laravel-mix 사용하기
- model, controller, migration 생성하기
- restful한 route 만들기
- view 생성하기
- 기능구현하기
- 글쓰기 폼작성
- 저장하기
- 글보기
- 글 수정하기 폼작성
- 글 업데이트하기
- 글목록 페이지
- 삭제하기
- 모델을 라우트에 바인딩 시키자
- 중복 제거
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'];
}