[점프투장고] 조회수 기능 추가 관련 질문

조회수 기능을 구현하였는데 이해가 안가는 부분이 있어서 질문글을 올립니다..
보시면

models.py


from django.db import models
from django.db.models import Count
from django.urls import reverse
from django.contrib.auth.models import User


class Category(models.Model):
    name = models.CharField(max_length=20, unique=True)
    description = models.CharField(max_length=200, null=True, blank=True)
    has_answer = models.BooleanField(default=True)  # 답변가능 여부

    def __str__(self):
    #     return self.name
        return self.description

    def get_absolute_url(self):
        return reverse('pybo:index', args=[self.name])


class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_question')
    subject = models.CharField(max_length=200)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(User, related_name='voter_question')  # 추천인 추가
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='category_question')
    seen_cnt = models.PositiveIntegerField(default=0)

    def __str__(self):
        return self.subject

    @staticmethod
    def order_by_so(question_list, so):
        if so == 'recommend':
            # aggretation, annotation에는 relationship에 대한 역방향 참조도 가능 (ex. Count('voter'))
            question_list = question_list.annotate(num_voter=Count('voter')).order_by('-num_voter', '-create_date')
        elif so == 'popular':
            question_list = question_list.annotate(num_answer=Count('answer')).order_by('-num_answer', '-create_date')
        else:  # so == 'recent':
            question_list = question_list.order_by('-create_date')

        return question_list

    def get_absolute_url(self):
        return reverse('pybo:detail', args=[self.id])
    # 조회수 기능 추가 
    @property
    def update_counter(self):
        self.seen_cnt = self.seen_cnt+1
        self.save()


class Answer(models.Model):
    author = models.ForeignKey(User, 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(User, related_name='voter_answer')  # 추천인 추가

    def __str__(self):
        return self.content

    @staticmethod
    def order_by_so(answer_list, so):
        # 정렬
        if so == 'recommend':

            answer_list = answer_list.annotate(num_voter=Count('voter')).order_by('-num_voter', '-create_date')
        else:  # so == 'recent':
            answer_list = answer_list.order_by('-create_date')

        return answer_list

    def get_page(self, so='recommend'):
        # https://stackoverflow.com/questions/1042596/get-the-index-of-an-element-in-a-queryset
        answer_list = Answer.order_by_so(self.question.answer_set.all(), so)

        index = 0
        for _answer in answer_list:
            index += 1
            if self == _answer:
                break

        return (index - 1)//5 + 1

    # def get_relative_url(self, so, page):
    #     return reverse('pybo:detail', args=[self.question.id]) + f'?page={page}&so={so}#answer_{self.id}'

    def get_absolute_url(self):
        return reverse('pybo:detail', args=[self.question.id]) + f'?page={self.get_page()}#answer_{self.id}'


class Comment(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_comment')
    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)

    def __str__(self):
        return self.content

    def get_absolute_url(self):
        if self.question:
            return reverse('pybo:detail', args=[self.question.id]) + '#comment_question_start'
        else:  # if self.answer:
            return reverse('pybo:detail', args=[self.answer.question.id]) + \
                   f'?page={self.answer.get_page()}#comment_{self.id}'  # todo comment_id 가능?

models.py에서 Question 클래스에

seen_cnt = models.PositiveIntegerField(default=0)

    # 조회수 기능 추가 
    @property
    def update_counter(self):
        self.seen_cnt = self.seen_cnt+1
        self.save()

을 추가하였는데
이렇다면 question_list.html에서 단순히 조회수를 조회하는 부분엔

{{ question.seen_cnt }}

를 사용하고 ,
게시글의 상세페이지를 클릭해서 들어갈땐 ( question_detail.html )

{{ question.update_counter }}

를 사용해야하는 것 아닌가요? 왜 이렇게하면 None이 출력되고

question_list.html와 question_detail.html 두곳에서 모두 {{ question.seen_cnt }}를 사용해야 정상동작하는지 이유를 모르겠습니다..

사용한 템플릿 : question_list.html

{% extends 'base.html' %}
{% load pybo_filter %}

{% block content %}
<nav id="sidebar" class="border-top border-secondary">
    <div class="list-group text-lg-start-center">
        {% for cat in category_list %}
            {% if cat == category %}
                <a class="rounded-0 list-group-item list-group-item-action list-group-item-light active"
                   href="{{ cat.get_absolute_url }}">{{ cat.description }}</a>
            {% else %}
                <a class="rounded-0 list-group-item list-group-item-action list-group-item-light"
                   href="{{ cat.get_absolute_url }}">{{ cat.description }}</a>
            {% endif %}
        {% endfor %}
    </div>
</nav>

<div class="row justify-content-between my-3">
    <div class="col-2">
        <select class="form-control so">
            <option value="recent" {% if so == 'recent' %}selected{% endif %}>최신순</option>
            <option value="recommend" {% if so == 'recommend' %}selected{% endif %}>추천순</option>
            <option value="popular" {% if so == 'popular' %}selected{% endif %}>답변순</option>
        </select>
    </div>
    <div class="col-6">
        <div class="input-group">
            <input type="text" id ="search_kw" class="form-control" value="{{ kw|default_if_none:'' }}">
            <div class="input-group-append">
                <button class="btn btn-outline-secondary" type="button" id="btn_search">찾기</button>
            </div>
        </div>
    </div>
</div>

<table class="table">
    <thead>
        <tr class="text-center thead-dark">
            <th>번호</th>
            <th>추천</th>
            <th style="width:50%">제목</th>
            <th>글쓴이</th>
            <th>조회</th>
            <th>작성일시</th>
        </tr>
    </thead>
    <tbody>
    {% if question_list %}
    {% for question in question_list %}
    <tr class="text-center">
        <td>
            <!-- 번호 = 전체건수 - 시작인덱스 - 현재인덱스 + 1 -->
            {{ question_list.paginator.count|sub:question_list.start_index|sub:forloop.counter0|add:1 }}
        </td>
        <td>
            {% if question.voter.all.count > 0 %}
                <span class="text-danger small mx-2">{{ question.voter.count }}</span>
                    <!-- question.voter.count와 완전히 같은 쿼리(SQL문 동일) -->
            {% else %}
                <span class="badge badge-light px-2 py-1" style="color:#ccc;">0</span>
            {% endif %}
        </td>
        <td class="text-left">
            <a href="{{ question.get_absolute_url }}">{{ question.subject }}</a>
            {% if question.answer_set.count > 0 %}
            <span class="text-danger small ml-2">{{ question.answer_set.count }}</span>
            {% endif %}
        </td>
        <td>
            <a href="{% url 'common:profile_base' question.author.id %}">{{ question.author.username }}</a>
        </td>
        <!-- 조회수 -->
        <td>{{ question.seen_cnt }}</td>
        <!--작성일-->
        <td>{{ question.create_date }}</td>
    </tr>
    {% endfor %}
    {% else %}
    <tr>
        <td colspan="3">질문이 없습니다.</td>
    </tr>
    {% endif %}
    </tbody>
</table>
<!-- 페이징처리 시작 -->
<ul class="pagination justify-content-center">
    <!-- 이전페이지 -->
    {% if question_list.has_previous %}
        <li class="page-item">
            <a class="page-link"  href="?page=1">처음</a>
        </li>
       <li class="page-item">
            <a class="page-link" href="?page={{ question_list.previous_page_number }}">이전</a>
        </li>
    {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">처음</a>
        </li>
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">이전</a>
        </li>
    {% endif %}
    <!-- 페이지리스트 -->
    {% for page_number in question_list.paginator.page_range %}
        {% if page_number >= question_list.number|add:-4 and page_number <= question_list.number|add:4 %}
            {% if page_number == question_list.number %}
            <li class="page-item active" aria-current="page">
                <a class="page-link"  href="?page={{ page_number }}">{{ page_number }}</a>
            </li>
            {% else %}
            <li class="page-item">
                <a class="page-link"  href="?page={{ page_number }}">{{ page_number }}</a>
            </li>
            {% endif %}
        {% endif %}
    {% endfor %}
    <!-- 다음페이지 -->
    {% if question_list.has_next %}
        <li class="page-item">
             <a class="page-link" href="?page={{ question_list.next_page_number }}">다음</a>
        </li>
        <li class="page-item">
             <a class="page-link" href="?page={{ question_list.paginator.count }}">끝</a>
        </li>
    {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
        </li>
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">끝</a>
        </li>
    {% endif %}
</ul>
<!-- 페이징처리 끝 -->
<a href="{% url 'pybo:question_create' category.name %}" class="btn btn-primary">질문 등록하기</a>

<form id="searchForm" method="get" action="{{ category.get_absolute_url }}">
    <input type="hidden" id="kw" name="kw" value="{{ kw|default_if_none:'' }}">
    <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'>
const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        document.getElementById('page').value = this.dataset.page;
        document.getElementById('searchForm').submit();
    });
});
const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
    document.getElementById('kw').value = document.getElementById('search_kw').value;
    document.getElementById('page').value = 1;  // 검색버튼을 클릭할 경우 1페이지부터 조회한다.
    document.getElementById('searchForm').submit();
});
</script>

<script type='text/javascript'>
    $(document).ready(function() {
        $(".page-link").on('click', function() {
            $("#page").val($(this).data("page"));
            $("#searchForm").submit();
        });

        $("#btn_search").on('click', function() {
            $("#kw").val($(".kw").val());
            $("#page").val(1);  // 검색 버튼을 클릭할 경우 1페이지부터 조회한다.
            $("#searchForm").submit();
        });

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

사용한 템플릿 : question_detail.html

{% extends 'base.html' %}
{% block content %}
{% load pybo_filter %}

<div class="container my-3">
    <!-- message 표시 -->
    {% 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">[{{category.description}}]{{ question.subject }}</h2>
    <!--조회수 -->
    <h3> 조회: {{ question.seen_cnt }} </h3>
    <div class="card my-3">
        <div class="card-body">
<!--            <div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>-->
                <div class="card-text">{{ question.content|mark }}</div>
            <div class="d-flex justify-content-end">
                {% if question.modify_date %}
                <div class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div>{{ question.modify_date }}</div>
                </div>
                {% endif %}
                <div class="badge bg-light text-dark p-2 text-start">
                    <div class="mb-2">{{ question.author.username }}</div>
                    <div>{{ question.create_date }}</div>
                </div>
            </div>
            <div class="my-3">
                <a href="javascript:void(0)" data-uri="{% url 'pybo:vote_question' question.id %}"
                   class="recommend btn btn-sm btn-outline-secondary"> 추천
                    <span class="badge rounded-pill bg-success">{{question.voter.count}}</span>
                </a>
                {% if request.user == question.author %}
                <a href="{% url 'pybo:question_modify' question.id   %}"
                   class="btn btn-sm btn-outline-secondary">수정</a>
                <a href="javascript:void(0)" class="delete btn btn-sm btn-outline-secondary"
                   data-uri="{% url 'pybo:question_delete' question.id  %}">삭제</a>
                {% endif %}
            </div>
        </div>
    </div>
    <!-- 답변 -->
    <h5 class="border-bottom my-3 py-2">{{question.answer_set.count}}개의 답변이 있습니다.</h5>
    {% for answer in answer_list %}
    <div class="card my-3">
        <div class="card-body">
<!-- 답변내용 -->
<!--            <div class="card-text" style="white-space: pre-line;">{{ answer.content }}</div>-->
            <div class="card-text" >{{ answer.content|mark }}</div>
            <div class="d-flex justify-content-end">
                {% if answer.modify_date %}
                <div class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div>{{ answer.modify_date }}</div>
                </div>
                {% endif %}
                <div class="badge bg-light text-dark p-2 text-start">
                    <div class="mb-2">{{ answer.author.username }}</div>
                    <div>{{ answer.create_date }}</div>
                </div>
            </div>
            <div class="my-3">
                <a href="javascript:void(0)" data-uri="{% url 'pybo:vote_answer' answer.id  %}"
                   class="recommend btn btn-sm btn-outline-secondary"> 추천
                    <span class="badge rounded-pill bg-success">{{answer.voter.count}}</span>
                </a>
                {% if request.user == answer.author %}
                <a href="{% url 'pybo:answer_modify' answer.id  %}"
                   class="btn btn-sm btn-outline-secondary">수정</a>
                <a href="javascript:void(0)" class="delete btn btn-sm btn-outline-secondary "
                   data-uri="{% url 'pybo:answer_delete' answer.id  %}">삭제</a>
                {% endif %}
            </div>
        </div>
    </div>
    {% endfor %}
    <!-- 답변 페이징처리 시작 -->
    <ul class="pagination justify-content-center">
        <!--처음페이지-->
        <li class="page-item">
            <a class="page-link" href="?page=1">처음</a>

        <!-- 이전페이지 -->
        {% if answer_list.has_previous %}
        <li class="page-item">
            <a class="page-link" href="?page={{ answer_list.previous_page_number }}">이전</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true"
               href="javascript:void(0)">이전</a>
        </li>
        {% endif %}
        <!-- 페이지리스트 -->
        {% for page_number in answer_list.paginator.page_range %}
        {% if page_number >= answer_list.number|add:-2 and page_number <= answer_list.number|add:2%}
        {% if page_number == answer_list.number %}
        <li class="page-item active" aria-current="page">
            <a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
        </li>
        {% else %}
        <li class="page-item">
            <a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
        </li>
        {% endif %}
        {% endif %}
        {% endfor %}
        <!-- 다음페이지 -->
        {% if answer_list.has_next %}
        <li class="page-item">
            <a class="page-link" href="?page={{ answer_list.paginator.num_pages }}">다음</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true"
               href="javascript:void(0)">다음</a>
        </li>
        {% endif %}
        <!--마지막페이지-->
        <li class="page-item">
            <a class="page-link" href="?page={{ answer_list.paginator.count }}">끝</a>
        </li>
    </ul>
    <!-- 답변 페이징처리 끝 -->

    <form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
        {% csrf_token %}
        {% include "form_errors.html" %}
        <div class="mb-3">
            <label for="content" class="form-label">답변내용</label>
            <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>
</div>
{% endblock %}
{% block script %}
<script type='text/javascript'>
const delete_elements = document.getElementsByClassName("delete");
Array.from(delete_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        if(confirm("정말로 삭제하시겠습니까?")) {
            location.href = this.dataset.uri;
        };
    });
});
const recommend_elements = document.getElementsByClassName("recommend");
Array.from(recommend_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        if(confirm("정말로 추천하시겠습니까?")) {
            location.href = this.dataset.uri;
        };
    });
});
</script>
{% endblock %}

flavonoid37 394

2023년 6월 4일 1:58 오전

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

안녕하세요.
개인적인 생각 몇자 적습니다.
틀리면 정정해주시면 감사하겠습니다.

파이썬에는 property라는 매력적인 기능이 있습니다.
하지만 그 매력적인 기능을 쓰기 위해선 property관련
docs(reference)를 최소한 1번은 봐야합니다.

  • https://docs.python.org/3/library/functions.html#property

현재 @property로 되어진 update_counter에는 return 해주는
값이 없습니다. 그래서 None 이 뜨는건 당연한 현상입니다.

해당 부분을 위주로 조지시면 답을 찾으실 수 있을 것 같습니다.
아니라면 죄송합니다.

by 로디(rodi)

로디

2023년 6월 7일 1:09 오후

아... 제가 궁금한점은 제가 올려놓은 코드를 보면 상세페이지를 클릭하였을 때 update_counter 메서드를 호출하지 않고 그냥 seen_cnt 만 사용하였는데 seen_cnt가 자동으로 올라가는 이유를 모르겠습니다. question_list.html 와 question_deail.html 모두 {{ question.seen_cnt }}로 조회수를 표시하는데 왜 상세페이지를 들어갈땐 update_counter가 적용되는지 모르겠습니다. - flavonoid37님, 2023년 6월 8일 9:02 오후 추천 , 대댓글
아 , question_deail.html 의 <!--조회수 증가 --> <!--{{question.update_counter }}--> 로 주석처리 해놓은게 적용되는 거였군요.. - flavonoid37님, 2023년 6월 8일 9:07 오후 추천 , 대댓글
소름이네요... 하나 배워 갑니다 :) - 로디님, 2023년 6월 9일 1:54 오후 추천 , 대댓글
@로디님 html 주석처리가 아니라 리퀴드 주석처리를 사용해야 주석처리가 되는 거였습니다. - flavonoid37님, 2023년 6월 10일 10:54 오후 추천 , 대댓글