마지막 기능으로 게시물에 댓글&대댓글 기능을 추가하고 이 프로젝트를 마무리 하려 한다.
db테이블은 원래 댓글테이블에 대댓글을 구분할수있는 컬럼을 추가해서 하나로 통합하려 했으나
관계테이블을 연습할겸 1:n관계 테이블로 대댓글 테이블을 나눠서
게시글(1)->댓글(n) 관계 1개
댓글(1)->대댓글(n) 관계 1개
로 구성해서 설계해봤다 laravel에 테이블 관계를 도와주는 기능을 활용했고
https://laravel.kr/docs/8.x/eloquent-relationships
이부분을 참고했다
먼저 프론트 작업먼저 진행했고 디자인은 유튜브 댓글 부분 레이아웃을 참고해서 비슷하게 만들어 보았다

먼저 기획한건 오른쪽에 메뉴는 댓글 작성자가 아니면 활성화되지않고 대댓글이없는 댓글은 n개의 답글 부분이 비활성화 된다. 그리고 한페이지에서 다 작동하도록 js와 ajax를 활용해서 기능을 구현했다
먼저 그냥 댓글은 동기 통신으로했고 대댓글은 ajax를 활용해서 비동기 통신으로 구현했다
댓글부분의 라우트와 컨트롤러 파트이다
//댓글 관련 라우팅
Route::middleware(['auth', 'verified'])->name('comment.')->prefix('comment')->group(function () {
Route::post('/', [CommentController::class, 'store'])->name('store');
Route::patch('/{comment}', [CommentController::class, 'update'])->name('update');
Route::delete('/{comment}/destroy', [CommentController::class, 'destroy'])->name('delete');
});
<?php
namespace App\Http\Controllers;
use App\Models\Comment;
use App\Models\Post;
use App\Models\Reply;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CommentController extends Controller
{
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//유효성 검사
$validatedData = $request->validate([
'content' => 'required|max:100',
]);
//엘로퀀트ORM이용해서 insert
$comment = Comment::create([
'content' => $validatedData['content'],
'writer' => Auth::user()->name,
'post_id' => $request->input('post_id'),
]);
if(!is_null($comment)){
return redirect()->back()->with('status', '댓글이 등록되었습니다.');
}
else{
return redirect()->back()->with('status', '댓글 등록에 실패했습니다.');
}
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Comment $comment
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Comment $comment)
{
//유효성 검사
$validatedData = $request->validate([
'comment_content' => 'required|max:100',
]);
//update
$comments = Comment::find($comment->id);
$comments->content = $validatedData['comment_content'];
$comments->save();
if(!is_null($comments)){
return redirect()->back()->with('status', '댓글이 수정되었습니다.');
}
else{
return redirect()->back()->with('status', '댓글 수정에 실패했습니다.');
}
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Comment $comment
* @return \Illuminate\Http\Response
*/
public function destroy(Comment $comment)
{
$comment->delete();
if(!is_null($comment)){
return redirect()->back()->with('status', '댓글이 삭제되었습니다');
}
else{
return redirect()->back()->with('status', '댓글 삭제에 실패했습니다');
}
}
그리고 게시글 파일에 댓글부분 html을 추가했다
{{-- 댓글작성 : 시작 --}}
<div class="flex flex-col w-9/12 mb-10 text-left">
<form action="{{ route('comment.store') }}" method="post" class="w-full max-w-xl bg-white rounded-lg px-4 pt-2">
@csrf
<input type="hidden" value="{{ $posts->id }}" name="post_id" id="post_id">
<div class="flex flex-wrap -mx-3 mb-6">
<h1 class="mb-6 text-xl font-semibold text-black">새 댓글 달기</h2>
<div class="w-full md:w-full mb-2 mt-2">
<textarea class="bg-gray-100 rounded border border-gray-400 leading-normal resize-none w-full h-20 py-2 px-3 font-medium placeholder-gray-700 focus:outline-none focus:bg-white" name="content" placeholder='내용을 입력해주세요.' required></textarea>
</div>
<div class="w-full md:w-full flex items-start md:w-full px-3">
<div class="flex items-start w-1/2 text-gray-700 px-2 mr-auto">
</div>
<div class="flex items-start w-1/2 text-gray-700 px-2 mr-auto">
</div>
<div class="-mr-1">
<input type='submit' class="bg-white text-gray-700 font-medium py-1 px-4 border border-gray-400 rounded-lg tracking-wide mr-1 hover:bg-gray-100" value='댓글 작성하기'>
</div>
</div>
</form>
</div>
{{-- 댓글작성 : 끝 --}}
{{-- 댓글리스트 : 시작 --}}
<div class="antialiased max-w-screen-sm">
<h3 class="mb-4 text-lg font-semibold text-gray-900">댓글 {{ $comments->count() }}개</h3>
@foreach ($comments as $comment)
<div class="space-y-4 mb-3">
<div class="flex">
<div class="flex-shrink-0 mr-3">
<img class="mt-2 rounded-full w-8 h-8 sm:w-10 sm:h-10" src="{{ asset('storage\default_profile.jpg') }}" alt="">
</div>
<div class="flex-1 border rounded-lg px-4 py-2 sm:px-6 sm:py-4 leading-relaxed">
<div class="relative mb-1">
<strong>{{ $comment->writer }}</strong>
<span class="text-xs text-gray-400">
{{ $comment->created_at->format('Y/m/d H:i') }}
@if ($comment->created_at != $comment->updated_at)
(수정됨)
@endif
</span>
@if (Auth::check() && $comment->writer == Auth::user()->name)
{{-- 드롭다운 id에 댓글 id를 추가함으로써 각자 다른 드롭다운으로 적용시킨다 --}}
<button id="dropdownButton_comment_{{ $comment->id }}" data-dropdown-toggle="dropdown_comment_{{ $comment->id }}"
class="text-gray-600 absolute right-0 mr-4 hover:bg-gray-100 font-medium rounded-full text-sm px-4 py-2.5 text-center inline-flex items-center"
type="button"><i class="fas fa-ellipsis-v"></i></button>
<form action="{{ route('comment.delete',['comment' => $comment]) }}" method="post" id="comment_del_{{ $comment->id }}">
@csrf
@method('DELETE')
<div id="dropdown_comment_{{ $comment->id }}" class="hidden z-10 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700">
<ul class="py-1 px-1" aria-labelledby="dropdownButton_comment_{{ $comment->id }}">
<li>
<a onclick="edit({{ $comment->id }})"
href="javascript://" class="block py-2 px-4 text-sm text-gray-700 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">수정</a>
</li>
<li>
<a onclick="del('comment_del_{{ $comment->id }}');"
href='javascript://' class="block py-2 px-4 text-sm text-gray-700 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" >삭제</a>
</li>
</ul>
</div>
</form>
@endif
<p class="text-sm">
<form action="{{ route('comment.update',['comment' => $comment]) }}" method="POST" id="">
@csrf
@method('PATCH')
<div class="relative z-0 mb-6 w-full group">
<p class="" id="comment_{{ $comment->id }}">{{ $comment->content }}</p>
<span name="comment_content" id="comment_content_{{ $comment->id }}" class="hidden flex">
<input type="text" name="comment_content" value="{{ $comment->content }}" class="block py-2.5 px-0 w-10/12 text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer" placeholder=" " required>
<button type="button" onclick="edit_cancel({{ $comment->id }})"><i class="fas fa-times"></i></button>
<button type="submit" class="ml-2"><i class="fas fa-level-down-alt fa-rotate-90"></i></button>
</span>
</div>
</form>
</p>
</div>
<div class="mt-4 flex items-center">
<div class="text-sm text-gray-500 font-semibold w-full">
<p class="hover:underline mb-1 w-1/10" id="reply_{{ $comment->id }}">
<a href="javascript:reply_create({{ $comment->id }})"><i class="fas fa-comment-dots"></i> 답글</a>
</p>
댓글 댓글 부분은 크게 막힘없이 완성했고
대댓글 부분에서 설계를 3번정도 다시 했던거같다 자꾸 놓치는 부분이있었고 기획의 중요성을 다시금 알게되는 부분이였다
대댓글의 라우트와 컨트롤러다
//대댓글 관련 라우팅
Route::middleware(['auth', 'verified'])->name('reply.')->prefix('reply')->group(function () {
Route::post('/', [CommentController::class, 'reply_store'])->name('store');
Route::patch('/update', [CommentController::class, 'reply_update'])->name('update');
Route::delete('/destroy', [CommentController::class, 'reply_destroy'])->name('delete');
});
public function reply_store(Request $request){
//유효성 검사
$validatedData = $request->validate([
'reply_content' => 'required|max:100',
]);
//엘로퀀트ORM이용해서 insert
$replys = Reply::create([
'content' => $validatedData['reply_content'],
'writer' => Auth::user()->name,
'comment_id' => $request->input('comment_id'),
]);
if(!is_null($replys)){
return redirect()->back()->with('status', '대댓글 작성에 성공했습니다');
}
else{
return redirect()->back()->with('status', '대댓글 작성에 실패했습니다');
}
}
public function reply_update(Request $request){
//유효성 검사
$validatedData = $request->validate([
'reply_content' => 'required|max:100',
]);
//update
$replys = Reply::find($request->input('id'));
$replys->content = $validatedData['reply_content'];
$replys->save();
if(!is_null($replys)){
return response()->json(array('reply'=> $replys), 200);
}
else{
return redirect()->back()->with('status', '대댓글 수정에 실패했습니다.');
}
}
public function reply_destroy(Request $request){
$replys = Reply::find($request->input('id'));
$replys->delete();
if(!is_null($replys)){
return response()->json(array('reply'=> $replys), 200);
}
else{
return redirect()->back()->with('status', '대댓글 수정에 실패했습니다.');
}
원래 테이블 관계를 이용해서 하려고 과도하게 욕심내다보니 꼬이는 부분이 많아서 타협한 부분도 있던거같다 결국엔 한번정도밖에 사용하지못했고 그부분은 html부분에서 다시 엘로퀀트로 해당 댓글의 대댓글을 불러오는 방식이다
{{-- 대댓글 입력 : start --}}
<form action="{{ route('reply.store') }}" method="post">
@csrf
<span name="reply_content" id="reply_content_{{ $comment->id }}" class="hidden flex">
<input type="text" id="reply_content" name="reply_content" value="" class="block py-2.5 px-0 w-10/12 text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer" placeholder="" required>
<button type="button" onclick="reply_create_cancel({{ $comment->id }})"><i class="fas fa-times"></i></button>
<button type="submit" class="ml-2" id="reply_create"><i class="fas fa-level-down-alt fa-rotate-90"></i></button>
<input type="hidden" value="{{ $comment->id }}" name="comment_id" id="comment_id">
</span>
</form>
{{-- 대댓글 입력 : end --}}
@php
$replys = App\Models\Comment::find($comment->id)->reply()->get();
@endphp
@if ($replys->count() > 0)
<p class="hover:underline text-blue-500">
<a href="javascript:reply_list({{ $comment->id }})"><i class="fas fa-chevron-right" id="reply_count_{{ $comment->id }}"></i> {{ $replys->count() }}개의 답글</a>
</p>
@endif
<div class='space-y-2 hidden' id="reply_list_{{ $comment->id }}">
@foreach ($replys as $reply)
<div class='flex' id="re_reply_one_{{ $reply->id }}">
<div class='flex-shrink-0 mr-3'>
<img class='mt-3 rounded-full w-6 h-6 sm:w-8 sm:h-8' src="{{ asset('storage\default_profile.jpg') }}" alt=''>
</div>
<div class='relative flex-1 bg-gray-100 rounded-lg px-4 py-2 sm:px-6 sm:py-4 leading-relaxed mt-4'>
<strong>{{ $reply->writer }}</strong>
<span class='text-xs text-gray-400'>{{ $reply->created_at->format('Y/m/d H:i') }}
@if ($reply->created_at != $reply->updated_at)
(수정됨)
@endif
</span>
@if (Auth::check() && $reply->writer == Auth::user()->name)
{{-- 드롭다운 id에 댓글 id를 추가함으로써 각자 다른 드롭다운으로 적용시킨다 --}}
<button id="dropdownButton_reply_{{ $reply->id }}" data-dropdown-toggle="dropdown_reply_{{ $reply->id }}"
class="text-gray-600 absolute right-0 mr-4 hover:bg-gray-100 font-medium rounded-full text-sm px-4 py-2.5 text-center inline-flex items-center"
type="button"><i class="fas fa-ellipsis-v"></i>
</button>
<form action="{{ route('reply.delete',['reply' => $reply]) }}" method="post" id="reply_del_{{ $reply->id }}">
@csrf
@method('DELETE')
<div id="dropdown_reply_{{ $reply->id }}" class="hidden z-10 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700">
<ul class="py-1 px-1" aria-labelledby="dropdownButton_reply_{{ $reply->id }}">
<li>
<a onclick="reply_edit({{ $reply->id }})"
href="javascript://" class="block py-2 px-4 text-sm text-gray-700 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">수정</a>
</li>
<li>
<a onclick="reply_delete({{ $reply->id }});"
href='javascript://' class="block py-2 px-4 text-sm text-gray-700 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" >삭제</a>
</li>
</ul>
</div>
</form>
@endif
<p class='text-sm sm:text-sm' id="re_reply_{{ $reply->id }}">
{{ $reply->content }}
</p>
<span name="reply_content" id="re_reply_content_{{ $reply->id }}" class="hidden flex">
<input type="text" id="reply_edit_content_{{ $reply->id }}" name="reply_edit_content_{{ $reply->id }}" value="{{ $reply->content }}" class="block py-2.5 px-0 w-10/12 text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer" placeholder="" required>
<button type="button" onclick="reply_edit_cancel({{ $reply->id }})"><i class="fas fa-times"></i></button>
<button type="button" class="ml-2" onclick="reply_update({{ $reply->id }})"><i class="fas fa-level-down-alt fa-rotate-90"></i></button>
</span>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
@endforeach
대댓글을 ajax로 구현했고 수정과 리스트를 태그교체를 이루는 부분에서 너무 비효율적인 작업을 요구해서 그냥 동기통신으로하고 수정과 삭제부분을 비동기로 구현했다
//대댓글 수정 ajax
function reply_update(id){
var content = $('input[name=reply_edit_content_'+id+']');
var reply_id = id;
$.ajax({
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
type: 'PATCH',
url: "{{ route('reply.update') }}",
dataType: 'json',
data: {
"reply_content" : content.val(),
"id" : id,
"_method" : "PATCH",
"status" : 'some status'
},
success: function(data) {
console.log(data);
$("#re_reply_"+reply_id).text(data.reply.content);
reply_edit_cancel(reply_id);
},
error: function(data) {
console.log(data);
alert("대댓글 수정에 실패했습니다");
}
});
}
//대댓글 삭제 ajax
function reply_delete(id){
var reply_id = id;
$.ajax({
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
type: 'DELETE',
url: "{{ route('reply.delete') }}",
dataType: 'json',
data: {
"id" : id,
"_method" : "DELETE",
"status" : 'some status'
},
success: function(data) {
console.log(data);
$("#re_reply_one_"+reply_id).remove();
},
error: function(data) {
console.log(data);
alert("대댓글 삭제에 실패했습니다");
}
});
laravel에서 관계와 ajax사용할때 주의할 점은 따로 포스트로 다루겠다
이렇게 해서 2달간의 프로젝트가 마무리되었다 솔직히 기능들만 보면 1달안에 충분히 끝낼만한 내용들이였지만 laravel을 처음부터 초심으로 돌아가 다시 공부하고 laravel의 장점과 기능들을 최대한 살리면서 기획하려고 애쓰다 보니 시간이 상당히 오래 걸린거같다 그리고 프로젝트 진행하면서 필요한 부분을 그때그때 공부하면서 하다보니 시간을 더 잡아먹은것도 있는거같다 그래도 역시 프로젝트만큼 언어나 프레임워크 공부에 도움이 되는건 없다는걸 다시한번 깨닫게 되는 프로젝트였다 이제 좀 더 기능들을 다듬고 다시한면 정리하면서 공부하는 과정을 거치면 될거같다
이프로젝트는 제 깃허브에 있고 하루하루 큰욕심없이 1일1기능 느낌으로 개발하니까 커밋량만 많아진거같다 다음 프로젝트는 커밋 하나하나에 무게감을 두는게 좋아보인다
https://github.com/bumpy1800/laravel-blog
GitHub - bumpy1800/laravel-blog
Contribute to bumpy1800/laravel-blog development by creating an account on GitHub.
github.com
'개발일지 > PHP' 카테고리의 다른 글
| 라라벨 블로그 만들기 #9(게시물 update&delete + 검색) (0) | 2022.02.07 |
|---|---|
| 라라벨 블로그 만들기 #8(게시물 list&show) (0) | 2022.02.03 |
| 라라벨 블로그 만들기 #7(게시물 작성) (0) | 2022.01.27 |
| 라라벨 블로그 만들기 #5(내정보, 사진변경, 이메일 인증) (0) | 2022.01.18 |
| 라라벨 블로그 만들기#4(로그인) (0) | 2022.01.12 |