Contents
django 11 - Reply(댓글) 기능 만들기 - CRUD 구성과 관계 설정까지 전부
   2022년06월21일     10분정도면 다 읽어요     - Comments

[카카오 클라우드 스쿨] 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 } }&nbsp;&nbsp;&nbsp;
        작성자: { { post.writer } }&nbsp;&nbsp;&nbsp;
        제목: { { post.title } }&nbsp;&nbsp;&nbsp;
        <a href="../delete/{ {post.id} }"> 삭제 </a>&nbsp;&nbsp;&nbsp;
        <a href="../update/{ {post.id} }"> 수정 </a>&nbsp;&nbsp;&nbsp;
        <a href="../list">      목록 </a>&nbsp;&nbsp;&nbsp;
        <hr/>
        { { post.contents } }

        <form action="/reply/create/{ { post.id } }" method="post">
            { % csrf_token % }
            { { replyForm } }
            <button> 댓글 입력 </button>
        </form>
    { % endif % }
</body>

4 . 정리

  1. 진작에 생성한 reply/forms.py 폼 모델이 있다
  2. board/views.py에 read() 함수: replyForms를 import 해서 context로 담아 html로 전달
  3. boardpan/read.html 에서 { { replyForm } } 입력 후, 댓글 입력
  4. action으로 인해 /reply/create/{ { post.id } } UID로 이동함
  5. replys.views.py에 create() 함수로 이동되고 실행됨

5 . 실행 결과

  • img_89
  • img_90
    • 2번 사용자가 46번 게시물에 댓글 단 것을 확인 가능



11-3. [아주 중요] 작성한 댓글 조회하기 (Read)

  • read.html 페이지에서 댓글을 작성했다면, read.html 페이지에서 확인할 수도 있어야지


select_related & prefetch_related (DB 용어)

  • 용도에 따라 두 가지 방식이 있다
    1. select_related 방식(정방향)
      • 기준이 되는 model class 안에 foreign key가 있을 경우 사용
      • 기준의 되는 model class 내부에 해당 앱에 대해 관계를 정의한 경우에 사용한다
    2. prefetch_related 방식(역방향)
      • 기준이 되는 model class 안에 foreign key가 없을 경우 사용
      • 기준이 되는 model class 외부에(해당 앱의 class에) 관계를 정의한 경우에 사용한다
    3. 구분
      • 쉽게 말해, foreign key가 어디 있는지에 따라 구분 가능
      • 즉, 1:n 등 관계 파악 -> foreign key 지정 -> 기능 구현의 기준을 정하기 -> 기준 모델에 foreign key 여부에 따라 결정한다
        • 예시)기준이 게시판이고, foreign key는 n측인 댓글에 있으므로, 기준이 되는 model class 안에 기능(foreign key)은 없으니까 prefetch related 방식이다
    4. 쉽게 말해 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 } }&nbsp;&nbsp;&nbsp;
        작성자: { { post.writer } }&nbsp;&nbsp;&nbsp;
        제목: { { post.title } }&nbsp;&nbsp;&nbsp;

        <!-- 링크 -->
        <a href="../delete/{ {post.id} }"> 삭제 </a>&nbsp;&nbsp;&nbsp;
        <a href="../update/{ {post.id} }"> 수정 </a>&nbsp;&nbsp;&nbsp;
        <a href="../list">      목록 </a>&nbsp;&nbsp;&nbsp;

        <!-- 본문 -->
        <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 } }&nbsp;&nbsp;&nbsp;
        작성자: { { post.writer } }&nbsp;&nbsp;&nbsp;
        제목: { { post.title } }&nbsp;&nbsp;&nbsp;

        <!-- 링크 -->
        <div align="left">
            <a href="../delete/{ {post.id} }"> 삭제 </a>&nbsp;&nbsp;&nbsp;
            <a href="../update/{ {post.id} }"> 수정 </a>&nbsp;&nbsp;&nbsp;
            <a href="../list">      목록 </a>&nbsp;&nbsp;&nbsp;
        </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 . 결과

  • img_138



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>&nbsp;&nbsp;&nbsp;
<a href="/reply/update/{ {reply.id} }"> 수정 </a>&nbsp;&nbsp;&nbsp;


참고: jekyll 블로그 특성항 템플릿 언어 { {, { % 등등이 적용이 안되어 불가피하게 공백을 삽입했어요
원래는 공백 있으면 안돼요