학습 정리 - Django (MTV모델, QuerySet 객체, Q함수, Anchor 구현) (2025.03.26)

수아·2025년 3월 26일
0

학습 정리

목록 보기
41/51
post-thumbnail

회고 리스트

1. MTV 모델에 대하여 설명하시오.

MTV(Model-Template-View)모델은 애플리케이션을 구조화하는 방식으로 MVC(Model-View-Controller) 패턴과 비슷하다.

MTV 패턴을 이용하는 이유는 세 가지이다.

  1. 역할이 분리되어 유지보수가 용이 - 데이터를 관리하는 Model, 로직을 처리하는 View, 화면을 구성하는 Template으로 나뉘어 있어 각 부분을 독립적으로 수정 가능
  2. Django의 강력한 ORM 지원 - SQL을 직접 작성하지 않고 Model을 통해 데이터 조작 가능
  3. 템플릿 엔진을 통한 HTML 렌더링 - {{ 변수명 }} 같은 템플릿 문법으로 동적인 페이지 쉽게 구현 가능

- Model

: 데이터 베이스와 직접 연결되는 부분
: 데이터 구조를 정의하고 데이터 저장 및 조회를 담당
: models.py에 정의하며 Django의 ORM을 통해 데이터베이스와 상호작용

- Template

: HTML을 담당하는 부분
: 데이터를 화면에 출력하는 역할
: Django의 템플릿 엔진({{ }} 문법)으로 동적인 HTML 생성 가능
: templates/ 폴더에 HTML 파일을 만들고 Django의 render() 함수로 연결

- View

: 사용자의 요청을 처리하고 적절한 응답을 반환
: Model에서 데이터를 가져와 Template에 전달하는 역할
: views.py에서 작성하며 Django의 request 객체를 받아서 처리
: urls.py와 연결되어 특정 URL 요청이 오면 실행

MTV와 MVC의 차이

MVC(Model-View-Controller)에서 Controller 역할을 Django에서는 View가 수행하고 View 역할을 Template이 수행한다.
Django에서는 MTV 패턴이지만 MVC와 개념적으로 동일한 구조이다.


2. QuerySet 객체에 대하여 설명하시오.

Django에서 QuerySet은 데이터베이스의 레코드 목록을 표현하는 객체로 데이터베이스에서 가져온 객체들의 집합이라고 보면 된다.

QuerySet의 특징

  • 지연 실행 (Lazy Evaluation)
    : QuerySet 객체는 즉시 실행되지 않고 필요할 때 실행된다.
    : .filter(), .exclude() 등을 호출해도 데이터베이스 쿼리가 즉시 실행되지 않고 최종적으로 평가될 때 실행된다.

  • 체이닝 (Chaining)
    : 여러 개의 .filter()나 .order_by() 같은 메서드를 연결해서 사용 가능하다.
    : 각 메서드는 새로운 QuerySet을 반환하므로 연속적으로 메서드를 호출할 수 있다.

  • 슬라이싱
    : QuerySet은 리스트처럼 슬라이싱이 가능하다.

QuerySet 생성 및 조회

QuerySet은 Django ORM을 통해 테이블에서 데이터를 가져올 때 생성되고 일반적으로 objexts 매니저를 사용해서 생성할 수 있다.

  • all( ) : 모든 데이터 조회
  • filter( ) : 조건을 적용한 데이터 조회
  • get( ) : 특정 데이터 조회
    (get( )은 특정 조건을 만족하는 단일 객체만 조회가능하여 결과가 없거나 여러 개면 오류 발생)
  • exclude( ) : 데이터 제외
  • order_by( ) : 정렬

from pybo.models import Question

queryset = Question.objects.all()  # Question 테이블의 모든 데이터 조회

qs = Question.objects.filter(subject="Django란?")
qs = Question.objects.filter(subject__icontains="Django")

q = Question.objects.get(id=1)

qs = Question.objects.exclude(subject="Django란?")

qs = Question.objects.order_by("create_date")  # 오래된 순 정렬
qs = Question.objects.order_by("-create_date")  # 최신 순 정렬 (내림차순)

get() vs filter()

get()단일객체만 반환하고 데이터가 없을 땐 DoesNotExist 오류 / 데이터가 여러 개라면 MultipleObjectsReturned 오류가 난다.

filter()QuerySet(객체 목록)을 반환하는 메서드이기 때문에 데이터가 없으면 빈 QuerySet을 반환 / 여러 개의 데이터가 있으면 여러 개를 모두 반환할 수 있다.

따라서 여러 개의 결과가 나올 가능성이 있다면 안전하게 filter() 사용하는 것을 추천한다.
맨 처음 나온 객체만 가져오고 싶은데 DoesNotExist 예외 처리가 귀찮다면 filter().first()를 사용하면 된다.

QuerySet 응용

  • or 조건 : Q( )함수
  • and 조건 : , 이나 &
  • COUNT : 객체 개수 반환
  • 슬라이싱 (페이징)
  • distinct( ) : 중복 제거
from django.db.models import Q

qs = Question.objects.filter(Q(subject__icontains="Django") | Q(content__icontains="Django"))

qs = Question.objects.filter(subject__icontains="Django", content__icontains="ORM")
qs = Question.objects.filter(Q(subject__icontains="Django") & Q(content__icontains="ORM"))

count = Question.objects.count()

qs = Question.objects.order_by("-create_date")[:5]  # 최신 질문 5개 가져오기

qs = Question.objects.filter(subject__icontains="Django").distinct()

QuerySet이 실제 SQL로 변환되는지 확인하고 싶다면

qs = Question.objects.filter(subject__icontains="Django")
print(qs.query)
# SELECT * FROM pybo_question WHERE subject LIKE '%Django%'

3. Q 함수에 대하여 예제를 들어 설명하시오.

Q 객체는 Django ORM에서 OR 조건을 적용하거나 복잡한 필터링을 수행할 때 사용하는 객체이다.
보통 .filter() 안에서는 ,(쉼표)로 조건을 연결하면 AND 조건이 되지만 OR 조건을 적용하려면 Q함수가 필요하다.

OR 조건 (| 연산자 사용)

from django.db.models import Q
from pybo.models import Question

qs = Question.objects.filter(Q(subject__icontains="Django") | Q(content__icontains="Python"))

# SELECT * FROM pybo_question 
# WHERE subject LIKE '%Django%' OR content LIKE '%Python%'

AND 조건 (& 연산자 사용)

qs = Question.objects.filter(Q(subject__icontains="Django") & Q(content__icontains="ORM"))
# qs = Question.objects.filter(subject__icontains="Django", content__icontains="ORM")

# SELECT * FROM pybo_question 
# WHERE subject LIKE '%Django%' AND content LIKE '%ORM%'

NOT 조건 (~ 연산자 사용)

qs = Question.objects.filter(~Q(subject__icontains="Django"))

# SELECT * FROM pybo_question
# WHERE subject NOT LIKE '%Django%'

복합 조건

qs = Question.objects.filter(
    (Q(subject__icontains="Django") | Q(content__icontains="Python")) & ~Q(author__username="admin")
)

# SELECT * FROM pybo_question
# WHERE (subject LIKE '%Django%' OR content LIKE '%Python%')
# AND author.username != 'admin'

4. pybo 프로젝트에서 Anchor 를 구현하시오.

참고

Django 프로젝트 pybo에서 Anchor(앵커) 기능을 구현하려면, HTML의 id 속성을 활용하여 특정 위치로 스크롤 이동하는 기능을 추가하면 된다.

답변 앵커 - HTML 수정

id="answer-{{ answer.id }}" → 각 답변에 고유한 id를 부여하여 해당 위치로 이동할 수 있도록 설정

<!-- pybo/question_detail.html -->

{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<div class="container my-3">
    <!-- message 표시 dev_17 -->
    {% 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="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
            
            <div class="d-flex justify-content-end">
                <!--dev_18-->
                {% 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 %}

                <!--dev_16-->
                <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>
            <!--로그인한 사용자와 글쓴이가 동일한 경우에만 노출 dev_17-->
            <div class="my-3">   
                <!-- dev_20 글 추천 -->
                <a href="javascript:void(0)" data-uri="{% url 'pybo:question_vote' 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 class="btn btn-sm btn-outline-secondary" href="{% url 'pybo:question_modify' question.id %}">수정</a>    
                    <a class="delete btn btn-sm btn-outline-secondary" href="javascript:void(0)" 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 question.answer_set.all %}
    <a id="answer_{{ answer.id }}"></a>
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;">{{ answer.content }}</div>
            <div class="d-flex justify-content-end">

                <!-- dev_18 -->
                {% 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 %}

                <!--dev_16-->
                <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>
            <!--dev_18-->
            <div class="my-3">     
                <!-- dev_20 답변 추천 -->
                <a href="javascript:void(0)" data-uri="{% url 'pybo:answer_vote' 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=" {% url 'pybo:answer_delete' answer.id %}" class="btn btn-sm btn-outline-secondary">삭제</a>
                {% endif %}     
           </div>

        </div>
    </div>
    {% endfor %}
    <!-- 답변 등록 -->
    <form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
        {% csrf_token %}
        <!-- 오류표시 Start {{form.as_p}}-->
        {% if form.errors  %}
            <div class="alert alert-danger" role="alert">
                {% for field in form %}
                {% if field.errors %}
                <div>
                    <strong>{{ field.label }}</strong>
                    {{ field.errors }}
                </div>
                {% endif %}
                {% endfor %}
            </div>            
        {% endif %}
        <!-- 오류표시 End -->
        <div class="mb-3">
            <label for="content" class="form-label">답변내용</label>
            <!-- dev_16 -->
            <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>

{% 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;
        };
    });
});

// dev_20
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 %}

답변 redirect

answer_views.py의 등록 / 수정 / 추천 함수에서

return redirect('pybo:detail', question_id=question.id)

위 코드를 아래 코드로 바꿔주면 된다.

return redirect(
	'{}#answer_{}'.format(
    resolve_url('pybo:detail', question_id=question.id), answer.id
    )
)

전체코드 ↓

# pybo/views/answer_views.py

from urllib import response
from django.http import HttpResponse, HttpResponseNotAllowed
from django.shortcuts import get_object_or_404, redirect, render, resolve_url

from pybo.forms import AnswerForm, QuestionForm
from pybo.models import Answer, Question
from django.utils import timezone
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from django.contrib import messages

# Create your views here.


# dev_9
@login_required(login_url="common:login")  # dev_16
def answer_create(request, question_id):
    """
    pybo 답변등록
    """

    question = get_object_or_404(Question, pk=question_id)

    if request.method == "POST":
        form = AnswerForm(request.POST)

        if form.is_valid():
            answer = form.save(commit=False)
            answer.author = request.user  # dev_16 현재 로그인한 계정의 User 모델 객체
            answer.create_date = timezone.now()
            answer.question = question
            answer.save()
            return redirect(
                "{}#answer_{}".format(
                    resolve_url("pybo:detail", question_id=question.id), answer.id
                )
            )

    else:
        return HttpResponseNotAllowed("Only POST is possible.")

    context = {"question": question, "form": form}
    return render(request, "pybo/question_detail.html", context)


# dev_18
@login_required(login_url="common:login")
def answer_modify(request, answer_id):

    answer = get_object_or_404(Answer, pk=answer_id)

    if request.user != answer.author:
        messages.error(request, "수정 권한이 없습니다.")
        return redirect("pybo:detail", question_id=answer.question.id)

    if request.method == "POST":
        form = AnswerForm(request.POST, instance=answer)

        if form.is_valid():
            answer = form.save(commit=False)
            answer.modify_date = timezone.now()
            answer.save()
            return redirect(
                "{}#answer_{}".format(
                    resolve_url("pybo:detail", question_id=answer.question.id),
                    answer.id,
                )
            )
    else:
        form = AnswerForm(instance=answer)

    context = {"answer": answer, "form": form}
    return render(request, "pybo/answer_form.html", context)


# dev_18
@login_required(login_url="common:login")
def answer_delete(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)

    if request.user != answer.author:
        messages.error(request, "삭제권한이 없습니다")
    else:
        answer.delete()

    return redirect("pybo:detail", question_id=answer.question.id)


# dev_20
@login_required(login_url="common:login")
def answer_vote(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user == answer.author:
        messages.error(request, "본인이 작성한 글은 추천할수 없습니다")
    else:
        answer.voter.add(request.user)
    return redirect(
        "{}#answer_{}".format(
            resolve_url("pybo:detail", question_id=answer.question.id), answer.id
        )
    )

0개의 댓글