[카카오 클라우드 스쿨] django 11 - Reply(댓글) 기능 만들기 - CRUD 구성과 관계 설정까지 전부
Reply(댓글) 기능 만들기 - CRUD 구성과 관계 설정까지 전부
- Boardpan과 관계를 맺을 것 1(게시물): N(댓글)
- User와 관계를 맺을 것 1(유저): N(댓글)
- 한 명의 유저는 여러 개의 댓글을 쓸 수 있다 O
- 하나의 댓글은 여러 유저가 수정 가능하다 X
1:n 관계의 경우, 관계를 맺는 설정은(foreign_key) n측에 넣어 준다
따라서 게시물, 사용자 와의 관계 모두 REPLY 측에 설정하면 됨
11-1. 앱 설치 후 model에서 관계 구성
1 . 앱 설치
- python manage.py startapp reply
2 . 앱 등록
- settings.py
3 . 모델에 클래스 만들기
- models.py
- 본문 입력 칸만 있음
- foreign 키로 게시물 앱 & 유저 앱과 관계 설정
class Reply(models.Model):
contents = models.TextField()
create_date = models.DateTimeField(auto_now_add=True)
# 게시물과 관계를 맺어 줌, DB에 저장된 Post만 들어올 수 있다
# cascade: 게시물 제거시 댓글 삭제
post = models.ForeignKey(Post, on_delete=models.CASCADE)
# User와 관계를 맺어 줌, DB에 저장된 User만 들어올 수 있다
# cascade: 유저가 회원 탈퇴시 댓글 제거 (존재하는 유저의 댓글만 저장가능)
writer = models.ForeignKey(User, on_delete=models.CASCADE)
4 . 클래스를 보고 입력받을 값(필드)을 forms.py에 적기 (댓글만임)
- forms를 썼으니, html에서 일일이 태그나 action을 지정해 주지 않아도 됨 (템플릿 문법 사용 가능)
class ReplyForm(forms.ModelForm):
class Meta:
model = Reply
fields = ('contents', )
exclude = ('writer', )
5 . 클래스 만들었으니 마이그레이션 하기
- python manage.py makemigrations reply
- python manage.py migrate
- 터미널에서 칼럼을 새로 추가할까요? 라고 묻는다면 나의 대답은
- 이번에도 1(직접 지정) - 1(ID1번 사용자)
- DB랑 연결 됨!
마이그레이션을 확인하고 되돌리는 방법
- python manage.py showmigrations
- 현재까지 진행된 마이그레이션을 출력해 본다 -> [X] 는 적용된 것
- python ./manage.py migrate reply(앱이름) 0001
- 0001번 마이그레이션으로 되돌리고 싶을 때 입력한다
- python ./manage.py migrate appname zero
- 테이블까지 싹다 없애버린다
- 취소를 하고자 하면 항상 DB에서 직접 하지 말고 django에서 해야 함
- 동기화가 안 맞게 됨!
- django.migrations 테이블에 마이그레이션 정보가 들어 감
- 동기화가 안 맞는다면 해당 테이블을 확인해 보자
6 . URL 설정
- (reply/read의 경우, 페이지와 기능이 따로 없고 boardpan/read에 구현할 예정
path('reply/create/<int:rid>', reply.views.create),
path('reply/list/<int:rid>', reply.views.list),
# path('reply/read/<int:rid>', reply.views.read),
path('reply/delete/<int:rid>', reply.views.delete),
path('reply/update/<int:rid>', reply.views.update),
11-2. 댓글 생성하기 (Create)
1 . reply/views.py - create()
# 게시물 조회 시 댓글도 같이 보이게 할 것이므로 GET은 필요 없음, 처리(POST)만 처리
@login_required(login_url='/user/login')
def create(request, rid):
if request.method == "POST":
# 사용자가 요청한 내용을 담은 객체
replyForm = ReplyForm(request.POST)
# 사용자가 유효하다(로그인)
if replyForm.is_valid():
reply = replyForm.save(commit=False)
# 작성자는 로그인한 사용자(내장모델), 이거 빼먹으면 is_valid에 의해 에러메시지 출력
# 유저 객체, 2번 사용자 넣고 싶다고 2를 쓰면 안됨 (제약 조건, user 내 데이터만 가능)
reply.writer = request.user
# 몇번 게시글인지 지정 (관계맺어준 변수는 해당 객체를 적어줘야 함)
post = Post() # 게시판 post모델의 객체 생성
post.id = rid # 게시글 번호 = 사용자가 전달한 번호(몇번 게시글의 댓글인지)
reply.post = post # 객체
# 저장
reply.save()
return redirect('/boardpan/read/' + str(rid))
2 . 관계를 맺어준 board/view.py - read() 수정 필요
# 게시글 하나를 조회한다(get), 파라미터로 bid값도 같이 줌
def read(request, bid):
# 조회하는 번호의 게시물 페이지를 준다
post = Post.objects.get(Q(id=bid))
# 댓글 입력 양식을 전달해 줌 (ReplyForm 클래스의 객체 생성)
replyForm = ReplyForm()
# 컨텍스트에 위 두개를 담는다
context = {'post': post, 'replyForm':replyForm}
# context값과 함께 클라이언트에 리턴
return render(request, 'boardpan/read.html', context)
3 . boardpan/read.html 도 고쳐야함 (게시물에서 댓글을 작성할 수 있도록)
- 컨텍스트에 담아준 replyForm을 출력시킨다
- form태그를 통해 댓글을 입력할 수 있도록 한다
- 전송 후 게시글 페이지로 이동, post 양식 form 사용
<body>
{ % if post % }
글번호: { { post.id } }
작성자: { { post.writer } }
제목: { { post.title } }
<a href="../delete/{ {post.id} }"> 삭제 </a>
<a href="../update/{ {post.id} }"> 수정 </a>
<a href="../list"> 목록 </a>
<hr/>
{ { post.contents } }
<form action="/reply/create/{ { post.id } }" method="post">
{ % csrf_token % }
{ { replyForm } }
<button> 댓글 입력 </button>
</form>
{ % endif % }
</body>
4 . 정리
- 진작에 생성한 reply/forms.py 폼 모델이 있다
- board/views.py에 read() 함수: replyForms를 import 해서 context로 담아 html로 전달
- boardpan/read.html 에서 { { replyForm } } 입력 후, 댓글 입력
- action으로 인해 /reply/create/{ { post.id } } UID로 이동함
- replys.views.py에 create() 함수로 이동되고 실행됨
5 . 실행 결과
- 2번 사용자가 46번 게시물에 댓글 단 것을 확인 가능
11-3. [아주 중요] 작성한 댓글 조회하기 (Read)
- read.html 페이지에서 댓글을 작성했다면, read.html 페이지에서 확인할 수도 있어야지
select_related & prefetch_related (DB 용어)
- 용도에 따라 두 가지 방식이 있다
- select_related 방식(정방향)
- 기준이 되는 model class 안에 foreign key가 있을 경우 사용
- 기준의 되는 model class 내부에 해당 앱에 대해 관계를 정의한 경우에 사용한다
- prefetch_related 방식(역방향)
- 기준이 되는 model class 안에 foreign key가 없을 경우 사용
- 기준이 되는 model class 외부에(해당 앱의 class에) 관계를 정의한 경우에 사용한다
- 구분
- 쉽게 말해, foreign key가 어디 있는지에 따라 구분 가능
- 즉, 1:n 등 관계 파악 -> foreign key 지정 -> 기능 구현의 기준을 정하기 -> 기준 모델에 foreign key 여부에 따라 결정한다
- 예시)기준이 게시판이고, foreign key는 n측인 댓글에 있으므로, 기준이 되는 model class 안에 기능(foreign key)은 없으니까 prefetch related 방식이다
- 쉽게 말해 1:n 관계설정 까지 구현하고 나서model.py를 보고 결정하면 됨 (내부에 기능이 있는지 없는지, 있으면 정방향, 없으면 역방향)
1 . 우선 reply/models.py 에서 다음과 같이 관계를 파악한다
- 게시물과 댓글은 1:n 관계이고, 댓글이 n측에 해당하므로, foreign_key는 reply에 있다
- 그리고 기능상 기준이 되는 모델은 ‘게시물’측이다.
- 따라서 기준이 되는 모델 안에 foreign_key가 없으므로 (해당 앱이 없으므로) prefetch related 방식으로 구현을 해야 한다
2 . POST_RELATED 방식 구현
boardpan/views.py - read()
- read 기능은 boardpan/read 페이지에 구현할 거라서, reply.read 대신 boardpan.read를 일부 고친다
- post 테이블에는 id, title, contents 등이 들어가 있었음
- prefetch_related를 사용하게 되면 reply_set 열이 추가되어 들어가게 된다
- SQL 기능적으로 이해한다면 쿼리는 2번이 실행된다
- POST table을 뽑아온다(reply 속성 포함) <- reply속성이 테이블이므로 Reply 테이블을 뽑아온다
# 게시글 하나를 조회한다(get), 파라미터로 bid값도 같이 줌
def read(request, bid):
# prefetch_related!!!!
# reply 라는 모델을 만들어 뒀었지, 얘랑 같이 뽑아볼 것이다
# reply는 여러 개(n)이므로 set 을 붙이도록 한다 (규칙)
post = Post.objects.prefetch_related('reply_set').get(Q(id=bid))
# 댓글 입력 양식을 전달해 줌 (ReplyForm 클래스의 객체 생성)
replyForm = ReplyForm()
# 컨텍스트에 위 두개를 담는다
context = {'post': post, 'replyForm':replyForm}
# context값과 함께 클라이언트에 리턴
return render(request, 'boardpan/read.html', context)
3 . read 페이지에서 댓글을 게시물과 함께 보여주기 위해서 다음과 같이 구성한다
read.html
<body>
<h1> 게시판 </h1>
<!-- 포스트 형식이라면 -->
{ % if post % }
<!-- 게시글 보기 -->
글번호: { { post.id } }
작성자: { { post.writer } }
제목: { { post.title } }
<!-- 링크 -->
<a href="../delete/{ {post.id} }"> 삭제 </a>
<a href="../update/{ {post.id} }"> 수정 </a>
<a href="../list"> 목록 </a>
<!-- 본문 -->
<hr/>
{ { post.contents } }
<!-- 댓글 달기 -->
<form action="/reply/create/{ { post.id } }" method="post">
{ % csrf_token % }
{ { replyForm } }
<button> 댓글 입력 </button>
</form>
<!-- 댓글 불러오기! 만약 post안에 reply_set이 들어있으면 다음을 수행한다 -->
{ % if post.reply_set % }
{ % for reply in post.reply_set.all % }
{ { reply.writer } } :
{ { reply.contents } }
<br>
{ % endfor % }
{ % endif % }
{ % endif % }
</body>
4 . 이쁘게 모양을 다듬는다
<body>
<h1> 게시판 </h1>
<!-- 포스트 형식이라면 -->
{ % if post % }
<!-- 게시글 보기 -->
글번호: { { post.id } }
작성자: { { post.writer } }
제목: { { post.title } }
<!-- 링크 -->
<div align="left">
<a href="../delete/{ {post.id} }"> 삭제 </a>
<a href="../update/{ {post.id} }"> 수정 </a>
<a href="../list"> 목록 </a>
</div>
<!-- 본문 -->
<hr/>
<h2> 본문 </h2>
<br/>
{ { post.contents } }
<hr/>
<!-- 만약 post안에 reply_set이 들어있으면 다음을 수행한다 -->
<h2> 댓글 보기 </h2>
<br>
{ % if post.reply_set % }
{ % for reply in post.reply_set.all % }
{ { reply.writer } } :
{ { reply.contents } }
<br>
{ % endfor % }
{ % endif % }
<br><br>
<!-- 댓글 달기 -->
<form action="/reply/create/{ { post.id } }" method="post">
{ % csrf_token % }
{ { replyForm } }
<button> 댓글 입력 </button>
</form>
<hr>
{ % endif % }
</body>
5 . 결과
11-4. 삭제 및 수정기능 추가 (Update, Delete)
- 댓글에도 수정 및 삭제가 가능하도록 조금 손 봐주면 완성!
1 . 댓글 삭제 기능 - reply/views.py
- 로그인이 필요하며, 본인이 아니면 댓글 삭제 불가
- 삭제 후 원래 있던 게시물로 리다이렉트
@login_required(login_url='/user/login')
def delete(request, rid):
# rid에 해당하는 댓글을 객체로 담음
reply = Reply.objects.get(id=rid)
# 댓글 작성자가 아니면 삭제 불가, 원래 게시물페이지로 이동 (테이블 id로 확인함)
if request.user.id != reply.writer_id:
return redirect('/boardpan/read/' + str(reply.post_id))
# 본인이 맞으면 삭제
reply.delete()
# 삭제가 완료되면 해당 댓글이 있던 게시글로 이동함
return redirect('/boardpan/read/' + str(reply.post_id))
2 . 댓글 수정 기능 - reply/views.py
- 로그인이 필요하며, 본인이 아니면 댓글 수정 불가
- 수정 페이지에서 수정 후 원래 있던 게시물로 리다이렉트
@login_required(login_url='/user/login')
def update(request, rid):
# rid에 해당하는 댓글을 객체로 담음
reply = Reply.objects.get(id=rid)
# 댓글 작성자가 아니면 수정 불가, 원래 게시물페이지로 이동 (테이블 id로 확인함)
if request.user.id != reply.writer_id:
return redirect('/boardpan/read/' + str(reply.post_id))
# 원래 적혀있던 양식 전달하기 (get)
if request.method == "GET":
# 불러온 객체를 양식에 맞게 사용자에게 보여줌
replyForm = ReplyForm(instance=reply)
# 입력하는 곳에 원래 적은 댓글 들어가있으라
context = {'replyForm': replyForm}
# 전달
return render(request, 'reply/create.html', context)
# 원래 서버(DB)에 있던 것을 대체해라!(post)
elif request.method == "POST":
# 불러온 객체를 대체하고 사용자가 올린 내용으로 DB에 올린다
replyForm = ReplyForm(request.POST, instance=reply)
# 유효하다면 저장함
if replyForm.is_valid():
reply = replyForm.save(commit=False)
reply.save()
# 원래 있던 게시물로 이동
return redirect('/boardpan/read/' + str(reply.post_id))
3 . boardpan/read.html에 댓글 삭제/수정 링크 추가
<a href="/reply/delete/{ {reply.id} }"> 삭제 </a>
<a href="/reply/update/{ {reply.id} }"> 수정 </a>
참고: jekyll 블로그 특성항 템플릿 언어 { {, { % 등등이 적용이 안되어 불가피하게 공백을 삽입했어요
원래는 공백 있으면 안돼요