[점프투장고] 3-16 추가기능 - 답변 페이징과 정렬

기존의 코드를 참고하여 답변 페이징 및 정렬을 완성했습니다.

다만, 실제 사용하다보니 최신순 정렬에서 2페이지 이후의 답변을 추천하거나 댓글을 달면 추천순으로 초기화된 후 1페이지로 바뀌는 문제가 발생했습니다. 이를 고치려고 우선 답변마다 달려있던 앵커들을 지우고 답변 목록 상단에 하나의 앵커(name=answer_start)만을 남겼습니다. (지금 이 사이트의 html 코드를 참고했습니다.) 그러나 이후 어떤 방식으로 구현해야되는지 모르겠어서 질문 남깁니다. 제 분석 및 문제점은 아래와 같습니다.

현재 추천 및 댓글 작동 방식:
1. question_detail.html에서 사용자가 추천 및 댓글 조작
2. 뷰에서는 각자의 컨트롤을 마친 후, pybo:detail로 리다이렉트

제 생각:
1에서, 템플릿 코드를 수정하여 GET 방식으로 추천 생성, 댓글 생성 뷰에 page와 so를 전달해야되는데 어떻게 구현할지 모르겠습니다.
2에서, 장고의 redirect 함수를 이용해 page와 so를 입력해주려면 어떻게 하는지, 가능한지 모르겠습니다.

코드는 아래와 같습니다.

question.html

{% extends 'base.html' %}
{% load pybo_filter %}
{% block content %}
<div class="container my-3">
    <!-- 사용자오류 표시 -->
    {% if messages %}
    <div class="alert alert-danger my-3" role="alert">
        {% for message in messages %}
        <strong>{{ message.tags }}</strong>
        <ul><li>{{ message.message }}</li></ul>
        {% endfor %}
    </div>
    {% endif %}
    <!-- 질문 제목 표시 -->
    <h2 class="border-bottom py-2">{{ question.subject }}</h2>
    <div class="row my-3">
        <div class="col-1">  <!-- 추천 영역 -->
            <div class="bg-light text-center p-3 border font-weight-bolder mb-1">{{ question.voter.count }}</div>
            <a href="#" data-uri="{% url 'pybo:vote_question' question.id %}"
               class="recommend btn btn-sm btn-secondary btn-block my-1">추천</a>
        </div>
        <div class="col-11">  <!-- 질문 영역 -->
            <div class="card">
                <div class="card-body">
                    <!-- 질문 내용 표시 -->
                    <div class="card-text">{{ question.content|mark }}</div>
                    <!-- 질문 생성&수정일시 표시 -->
                    <div class="d-flex justify-content-end">
                        {% if question.modify_date %}
                        <div class="badge badge-light p-2 text-left mx-3">
                            <div class="mb-2">modified at</div>
                            <div>{{ question.modify_date }}</div>
                        </div>
                        {% endif %}
                        <div class="badge badge-light p-2 text-left">
                            <div class="mb-2">{{ question.author.username }}</div>
                            <div>{{ question.create_date }}</div>
                        </div>
                    </div>
                    <!-- 질문 수정&삭제 버튼 표시 -->
                    {% if request.user == question.author %}
                    <div class="my-3">
                        <a href="{% url 'pybo:question_modify' question.id %}"
                           class="btn btn-sm btn-outline-secondary">수정</a>
                        <a href="#" class="delete btn btn-sm btn-outline-secondary"
                           data-uri="{% url 'pybo:question_delete' question.id %}">삭제</a>
                    </div>
                    {% endif %}
                    <!-- 질문 댓글 Start -->
                    {% if question.comment_set.count > 0 %}
                    <div class="mt-3">
                        {% for comment in question.comment_set.all %}  <!-- 등록한 댓글을 출력 -->
                        <a name="comment_{{ comment.id }}"></a>
                        <div class="comment py-2 text-muted">  <!-- 댓글 각각에 comment 스타일 지정 -->
                            <span style="white-space: pre-line;">{{ comment.content }}</span>
                            <span
                                - {{ comment.author }}, {{ comment.create_date }}
                                {% if comment.modify_date %}
                                (수정:{{ comment.modify_date }})
                                {% endif %}
                            </span>
                            {% if request.user == comment.author %}
                            <a href="{% url 'pybo:comment_modify_question' comment.id %}" class="small">수정</a>,
                            <a href="#" class="small delete" data-uri="{% url 'pybo:comment_delete_question' comment.id %}">삭제</a>
                            {% endif %}
                        </div>
                        {% endfor %}
                    </div>
                    {% endif %}
                    <div>
                        <a href="{% url 'pybo:comment_create_question' question.id %}"
                           class="small"><small>댓글 추가 ..</small></a>  <!-- 댓글 추가 링크 -->
                    </div>
                    <!-- 질문 댓글 End -->
                </div>
            </div>
        </div>
    </div>
    <!-- 답변 표시 Start -->
    <a name="answer_start"></a>
    <div class="row justify-content-between border-bottom my-3">
        <div class="col-4 py-2">
            <h5>{{ answer_set.paginator.count }}개의 답변이 있습니다.</h5>
        </div>
        <div class="col-2">
            <select class="form-control so">
                <option value="recommend" {% if so == 'recommend' %}selected{% endif %}>추천순</option>
                <option value="recent" {% if so == 'recent' %}selected{% endif %}>최신순</option>
            </select>
        </div>
    </div>
    {% for answer in answer_set %}
    <div class="row my-3">
        <div class="col-1">  <!-- 추천 영역 -->
            <div class="bg-light text-center p-3 border font-weight-bolder mb-1">{{ answer.voter.count }}</div>
            <a href="#" data-uri="{% url 'pybo:vote_answer' answer.id %}"
               class="recommend btn btn-sm btn-secondary btn btn-block my-1">추천</a>
        </div>
        <div class="col-11">  <!-- 답변 영역 -->
            <div class="card">
                <div class="card-body">
                    <!-- 답변 내용 표시 -->
                    <div class="card-text">{{ answer.content|mark }}</div>
                    <!-- 답변 일시&저자 표시 -->
                    <div class="d-flex justify-content-end">
                        {% if answer.modify_date %}
                        <div class="badge badge-light p-2 text-left mx-3">
                            <div class="mb-2">modified at</div>
                            <div>{{ answer.modify_date }}</div>
                        </div>
                        {% endif %}
                        <div class="badge badge-light p-2 text-left">
                            <div class="mb-2">{{ answer.author.username }}</div>
                            <div>{{ answer.create_date }}</div>
                        </div>
                    </div>
                    <!-- 질문 수정&삭제 버튼 표시 -->
                    {% if request.user == answer.author %}
                    <div class="my-3">
                        <a href="{% url 'pybo:answer_modify' answer.id %}"
                           class="btn btn-sm btn-outline-secondary">수정</a>
                        <a href="#" class="delete btn btn-sm btn-outline-secondary"
                           data-uri="{% url 'pybo:answer_delete' answer.id %}">삭제</a>
                    </div>
                    {% endif %}
                    <!-- 답변 댓글 Start -->
                    {% if answer.comment_set.count > 0 %}
                    <div class="mt-3">
                        {% for comment in answer.comment_set.all %}  <!-- 등록한 댓글을 출력 -->
                        <a name="comment_{{ comment.id }}"></a>
                        <div class="comment py-2 text-muted">  <!-- 댓글 각각에 comment 스타일 지정 -->
                            <span style="white-space: pre-line;">{{ comment.content }}</span>
                            <span
                                - {{ comment.author }}, {{ comment.create_date }}
                                {% if comment.modify_date %}
                                (수정:{{ comment.modify_date }})
                                {% endif %}
                            </span>
                            {% if request.user == coment.author %}
                            <a href="{% url 'pybo:comment_modify_answer' comment.id %}" class="small">수정</a>,
                            <a href="#" class="small delete" data-uri="{% url 'pybo:comment_delete_answer' comment.id %}">삭제</a>
                            {% endif %}
                        </div>
                        {% endfor %}
                    </div>
                    {% endif %}
                    <div>
                        <a href="{% url 'pybo:comment_create_answer' answer.id %}"
                           class="small"><small>댓글 추가 ..</small></a>  <!-- 댓글 추가 링크 -->
                    </div>
                    <!-- 답변 댓글 End -->
                </div>
            </div>
        </div>
    </div>
    {% endfor %}
    <!-- 답변 표시 End -->
    <!-- 페이징처리 시작 -->
    <ul class="pagination justify-content-center">
        <!-- 처음페이지 -->
        <li class="page-item">
            <a class="page-link" data-page="1" href="#">처음</a>
        </li>
        <!-- 이전페이지 -->
        {% if answer_set.has_previous %}
        <li class="page-item">
            <a class="page-link" data-page="{{ answer_set.previous_page_number }}" href="#">이전</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">이전</a>
        </li>
        {% endif %}
        <!-- 페이지리스트 -->
        {% for page_number in answer_set.paginator.page_range %}
            {% if page_number >= answer_set.number|add:-4 and page_number <= answer_set.number|add:4 %}
                {% if page_number == answer_set.number %}
                <li class="page-item active" aria-current="page">
                    <a class="page-link" data-page="{{ page_number }}" href="#">{{ page_number }}</a>
                </li>
                {% else %}
                <li class="page-item">
                    <a class="page-link" data-page="{{ page_number }}" href="#">{{ page_number }}</a>
                </li>
                {% endif %}
            {% endif %}
        {% endfor %}
        <!-- 다음페이지 -->
        {% if answer_set.has_next %}
        <li class="page-item">
             <a class="page-link" data-page="{{ answer_set.next_page_number }}" href="#">다음</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
        </li>
        {% endif %}
    </ul>
    <!-- 페이징처리 끝 -->
    <!-- 답변 등록폼 Start -->
    <form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
        {% csrf_token %}
        <!-- 오류표시 Start -->
        {% if form.errors %}
        <div class="alert alert-danger" role="alert">
            {% for field in form %}
                {% if field.errors %}
                <strong>{{ field.label }}</strong>
                {{ field.errors }}
                {% endif %}
            {% endfor %}
        </div>
        {% endif %}
        <!-- 오류표시 End -->
        <div class="form-group">
            <textarea {% if not user.is_authenticated %} disabled {% endif %}
                      name="content" id="content" class="form-control" rows="10"></textarea>
        </div>
            <input type="submit" value="답변등록" class="btn btn-primary">
    </form>
    <!-- 답변 등록폼 End -->
</div>
<form id="searchForm" method="get" action="{% url 'pybo:detail' question.id %}">
    <input type="hidden" id="page" name="page" value="{{ page }}">
    <input type="hidden" id="so" name="so" value="{{ so }}">
</form>
{% endblock %}
{% block script %}
<script type='text/javascript'>
    $(document).ready(function(){
        $(".delete").on('click', function() {
            if(confirm("정말 삭제하시겠습니까?")) {
                location.href = $(this).data('uri');
            }
        });
        $(".recommend").on('click', function() {
            if(confirm("정말 추천하시겠습니까?")) {
                location.href = $(this).data('uri');
            }
        });

        $(".page-link").on('click', function() {
            $("#page").val($(this).data("page"));
            $("#searchForm").submit();
        });
        $(".so").on('change', function() {
            $("#so").val($(this).val());
            $("#page").val(1);  // 새로운 기준으로 정렬할 경우 1페이지부터 조회한다.
            $("#searchForm").submit();
        });
    });
</script>
{% endblock %}

vote_views.py

(...생략...)

@login_required(login_url='common:login')
def vote_answer(request, answer_id):
    """
    pybo 답변추천등록
    """
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user == answer.author:
        messages.error(request, '본인이 작성한 글을 추천할 수 없습니다.')
    else:
        answer.voter.add(request.user)
    # todo 추천 시 답변 목록의 페이지 유지하기
    return redirect(f"{resolve_url('pybo:detail', question_id=answer.question.id)}#answer_start")

steven3391 81

M 2022년 1월 10일 1:20 오후

목록으로
1개의 답변이 있습니다. 1 / 1 Page

안녕하세요.

pybo.kr은 모델에 get_absolute_url 을 구현하여 사용했습니다.
댓글의 경우 다음과 같이 작성했으니 참고하시기 바랍니다.

class Answer(models.Model):
    author = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='author_answer')
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(CustomUser, related_name='voter_answer', blank=True)

    def get_absolute_url(self):
        # 현재 답변이 작성된 페이지 정보가 필요
        answer_index = self.get_index()
        page, r = divmod(answer_index, 10)
        if r != 0:
            page += 1
        return reverse('pybo:question_detail', args=[self.question.id]) + "?page=%s#answer_%s" % (page, self.id)

    def get_index(self):
        queryset = self.question.answer_set.order_by('create_date')
        numbered_qs = queryset.extra(select={
            'queryset_row_number': 'ROW_NUMBER() OVER (ORDER BY "id")'
        })
        with connection.cursor() as cursor:
            cursor.execute(
                "WITH OrderedQueryset AS (" + str(numbered_qs.query) + ") "
                "SELECT queryset_row_number FROM OrderedQueryset WHERE id = %s",
                [self.id]
            )
            index = cursor.fetchall()[0][0]

        # index = 0
        # for _answer in self.question.answer_set.all().order_by('create_date'):
        #     index += 1
        #     if self.id == _answer.id:
        #         break

        return index

    def get_recent_comments(self):
        return self.comment_set.all().order_by('-create_date')[:5]


class Comment(models.Model):
    author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    question = models.ForeignKey(Question, null=True, blank=True, on_delete=models.CASCADE)
    answer = models.ForeignKey(Answer, null=True, blank=True, on_delete=models.CASCADE)
    voter = models.ManyToManyField(CustomUser, related_name='voter_comment')

    class Meta:
        ordering = ['create_date']

    def get_question(self):
        if self.question:
            return self.question
        elif self.answer:
            return self.answer.question
        else:
            return None

    def get_absolute_url(self):
        if self.question:
            _anchor = "#anchor_question_all_comments"
            return reverse('pybo:question_detail', args=[self.question.id]) + _anchor
        elif self.answer:
            # 현재 답변이 작성된 페이지 정보가 필요
            answer_index = self.answer.get_index()
            page, r = divmod(answer_index, 10)
            if r != 0:
                page += 1
            _anchor = "?page=%s#anchor_answer_all_comments_%s" % (page, self.answer.id)
            return reverse('pybo:question_detail', args=[self.answer.question.id]) + _anchor
        else:
            return None

박응용

M 2022년 1월 10일 2:44 오후

답변 감사합니다! get_absolute_url 메소드에 대해서 새롭게 알 수 있었습니다. 다만 궁금한 점이 추가로 있습니다. Answer 클래스의 get_index 함수를 보면 'create_date'를 기준으로 정렬된 answer 리스트로부터 page를 구하는 것으로 이해했는데요, 만약 답변을 추천순으로 출력할 경우 페이지 번호가 상이할 수도 있어 보이는데 어떤 원리로 추천순에서도 페이지 번호가 잘 나오는지 궁금합니다. - steven3391님, 2022년 1월 11일 2:15 오후 추천 , 대댓글
@steven3391님 어이쿠, 버그가 있었네요, 현재 id 순으로 정렬하니까 "시간순" 정렬로 페이징 처리가 되겠네요.. 디폴트는 추천순이니 쿼리를 변경하던가 아니면 get_absolute_url은 so파라미터를 전달하여 시간순으로 정렬하던가 선택해야 겠네요. 파이보도 확인하고 수정해야 겠습니다. 알려주셔서 감사합니다. - 박응용님, 2022년 1월 11일 5:46 오후 추천 , 대댓글
@박응용님 확인해주셔서 감사합니다! 저도 한 번 구현해봐야겠네요 - steven3391님, 2022년 1월 12일 4:28 오후 추천 , 대댓글