
MTV(Model-Template-View)모델은 애플리케이션을 구조화하는 방식으로 MVC(Model-View-Controller) 패턴과 비슷하다.
: 데이터 베이스와 직접 연결되는 부분
: 데이터 구조를 정의하고 데이터 저장 및 조회를 담당
: models.py에 정의하며 Django의 ORM을 통해 데이터베이스와 상호작용
: HTML을 담당하는 부분
: 데이터를 화면에 출력하는 역할
: Django의 템플릿 엔진({{ }} 문법)으로 동적인 HTML 생성 가능
: templates/ 폴더에 HTML 파일을 만들고 Django의 render() 함수로 연결
: 사용자의 요청을 처리하고 적절한 응답을 반환
: Model에서 데이터를 가져와 Template에 전달하는 역할
: views.py에서 작성하며 Django의 request 객체를 받아서 처리
: urls.py와 연결되어 특정 URL 요청이 오면 실행
MVC(Model-View-Controller)에서 Controller 역할을 Django에서는 View가 수행하고 View 역할을 Template이 수행한다.
Django에서는 MTV 패턴이지만 MVC와 개념적으로 동일한 구조이다.
Django에서 QuerySet은 데이터베이스의 레코드 목록을 표현하는 객체로 데이터베이스에서 가져온 객체들의 집합이라고 보면 된다.
지연 실행 (Lazy Evaluation)
: QuerySet 객체는 즉시 실행되지 않고 필요할 때 실행된다.
: .filter(), .exclude() 등을 호출해도 데이터베이스 쿼리가 즉시 실행되지 않고 최종적으로 평가될 때 실행된다.
체이닝 (Chaining)
: 여러 개의 .filter()나 .order_by() 같은 메서드를 연결해서 사용 가능하다.
: 각 메서드는 새로운 QuerySet을 반환하므로 연속적으로 메서드를 호출할 수 있다.
슬라이싱
: QuerySet은 리스트처럼 슬라이싱이 가능하다.
QuerySet은 Django ORM을 통해 테이블에서 데이터를 가져올 때 생성되고 일반적으로 objexts 매니저를 사용해서 생성할 수 있다.
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()은 단일객체만 반환하고 데이터가 없을 땐 DoesNotExist 오류 / 데이터가 여러 개라면 MultipleObjectsReturned 오류가 난다.
filter()는 QuerySet(객체 목록)을 반환하는 메서드이기 때문에 데이터가 없으면 빈 QuerySet을 반환 / 여러 개의 데이터가 있으면 여러 개를 모두 반환할 수 있다.
따라서 여러 개의 결과가 나올 가능성이 있다면 안전하게 filter() 사용하는 것을 추천한다.
맨 처음 나온 객체만 가져오고 싶은데 DoesNotExist 예외 처리가 귀찮다면 filter().first()를 사용하면 된다.
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()
qs = Question.objects.filter(subject__icontains="Django")
print(qs.query)
# SELECT * FROM pybo_question WHERE subject LIKE '%Django%'

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

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