
pyenv virtualenv 3.12.1 oz_blog | 새로운 가상 환경 생성pyenv local oz_blog | oz_blog 가상환경을 활성화poetry init : poetry 설정 poetry add django : django 설치poetry add django == 5.0.4 : 버전 고정 설치poetry add django ~= 5.0 : 5.0버전에서 최신버전 설치django-admin startproject config .. 은 현재 경로에서 파일을 만들겠다는 의미

poetry add ipython / python manage.py shellipython — 더 똑똑한 파이썬 쉘
poetry django-extensions / python manage.py shell_plusimport shellmodels 자동 불러오기 (매번 from app.models import User 안 해도 됨)python manage.py show_urls | URL 목록 보기python manage.py graph_models -a -o erd.pngpython manage.py makemigrations / python manage.py migrate
python manage.py createsuperuser | 관리자 계정 생성


def __str__(self)get_category_display()[free] 가 나와야 하지만 [자유] 가 나오도록 변경'choices=' 때문에 CharField 임에도 선택박스가 나오는 것python manage.py startapp blogsettings 의 INSTALLED_APPS 에 등록


get_object_or_404: 없는 아이디값을 출력하면 404 반환


<a href="{% url 'blog_detail' blog.pk %}">blog_detail 을 가진 path를 찾음blog.pkpath('blog/<int:pk>/', views.blog_detail,name='blog_detail')<int:pk>) 로 들어오는 숫자를 그냥 blog.pk 가 받는것<a href="{% url 'blog_list' %}">

visits = int(request.COOKIES.get('visits',0)) +1getreturn 되는 값이 str이기 때문에 int로 변환accounts 의 기능들 확인admin 페이지에서 만든 superuser로 로그인 함


csrfsettings.py MIDDLEWARE 에 설정되어있어 모든 Post에서 CSRF 토큰을 검증crsf 토큰을 주지 않으면 검증에 실패하여 Post 요청이 들어가지 않음

python manage.py startapp member
UserCreationForm
settings 에 존재하고,form = UserCreationForm(request.POST)Form 에 request.POST 를 넣어주면 자동으로 validation 됨


form = UserCreationForm(request.POST or None)post 일 경우 post데이터가 들어가고 아닐경우 none데이터가 들어감
from django.conf import settings (권장)from config import settings
redirect(reverse('blog_list'))from django.urls import reversereverse : 이름을 가지고 어떤 url로 갈지 찾아줌config/urls.py 에서 blog_list 라는 이름을 가진 url을 찾고 redirectauthor = models.ForeignKey(User, on_delete=models.CASCADE)models.CASCADE -> 같이 삭제models.PROTECT -> 삭제가 불가능함 models.SET_NULL -> null값을 넣음
{% block content %}{% endblock %}
Userfrom django.contrib.auth.models import Userfrom django.contrib.auth import get_user_modelUser = get_user_model()django.contrib.auth.models의 User을 사용은 Xpython manage.py makemigrations / python manage.py migrate

from django import forms
from blog.models import Blog
class BlogForm(forms.ModelForm):
class Meta:
model = Blog
fields = ('title', 'content', )
class BlogForm(forms.ModelForm):fields__all__ / 특정 컬럼: list , dictfrom blog.forms import BlogForm
def blog_create(request):
form = BlogForm(request.POST or None)
if form.is_valid():
blog = form.save(commit=False)
blog.author = request.user
blog.save()
return redirect(reverse('blog_detail', kwargs={'pk': blog.pk}))
context = {'form': form}
return render(request, 'blog_create.html', context)
form = BlogForm(request.POST or None)BlogFormModelForm 을 기반으로 한 폼 클래스if form.is_valid():blog = form.save(commit=False) commit=False 덕분에 DB 저장 전에 추가로 author를 지정할 수 있게 됨blog.author = request.user blog.save()blog.pk(기본키, ID) 가 생김return redirect(reverse('blog_detail', kwargs={'pk': blog.pk}))reverse('blog_detail', kwargs={'pk': blog.pk})context = {'form': form}
return render(request, 'blog_create.html', context)
blog_create.html 템플릿을 렌더링해서 사용자에게 폼을 보여줌context 에 form 을 담아 템플릿에서 {{ form.as_p }}처럼 사용 가능
'/accounts/login/'http://127.0.0.1:8000/accounts/login/?next=/create/next=/create/
# member/views.py
def login(request):
form = AuthenticationForm(request, request.POST or None)
if form.is_valid():
django_login(request, form.get_user())
next = request.GET.get('next')
if next:
return redirect(next)
return redirect(reverse('blog_list'))
else:
form = AuthenticationForm(request)
context = {
'form': form
}
return render(request, 'registration/login.html', context)
next = request.GET.get('next')request.GET['next'] 도 가능하지만 이거는 없으면 오류발생 함http://127.0.0.1:8000/login/?next=/create/ | '?' 뒤에있는 next를 받는 것
-created_at : DESC / created_at : ASC

if request.user == blog.authorinstance=blog


request.GET: 쿼리스트링을 가져옴http://127.0.0.1:8000/?page=2 와 같이 물음표 뒤의 쿼리문으로 페이지를 이동page=0: 마지막 페이지로 이동

page_object.number : 현재 페이지

for i in page_object.paginator.page_rangeelif i > page_object.number|add:-3 and i < page_object.number|add:3
... 누르면 현재페이지 + 3 페이지로 가짐<a href="?page={{ page_object.number|add:-3 }}">…</a><a href="?page={{ page_object.number|add:3 }}">…</a>
if page_object.has_previous
if page_object.has_next


q = request.GET.get('q')
if q:
blogs = blogs.filter(
Q(title__icontains=q) |
Q(content__icontains=q)
)
제목과 본문 모두 검색 대상으로 설정

다른 페이지 번호를 눌렀을 때 전체 블로그 글이 보여지는 부분을 수정

# 1
if request.method == 'POST':
raise Http404
# 2
@require_http_methods(['POST'])
@require_http_methods(['POST']) csrf_token 필요

| Class-Based Views 공식문서 | generic view |
ListView, DetailView, CreateView)를 사용해 일반적인 패턴을 쉽게 구현지금까지 했던 방법

한줄로 처리 가능

이전보다 좀더 간결하게 변경

어바웃 페이지로 이동하는 리다이렉트 뷰(기본 방법 / 익명함수 lambda를 사용하는 방법)
path('redirect/', RedirectView.as_view(pattern_name='about'), name='redirect')
http://127.0.0.1:8000/redirect/ pattern_name과 동일 이름으로 이동

# blog/cb_views.py
from django.db.models import Q
from django.views.generic import ListView
from blog.models import Blog
class BlogListView(ListView): # ListView 상속
queryset = Blog.objects.all()
template_name = 'blog_list.html' # 렌더링
paginate_by = 10 # 페이지네이션
ordering = ('-created_at', ) # 역정렬
# ListView 내장 함수
# 검색기능
def get_queryset(self):
queryset = super().get_queryset()
q = self.request.GET.get('q')
if q:
queryset = queryset.filter(
Q(title__icontains=q) |
Q(content__icontains=q)
)
return queryset


페이지네이션 / 역정렬 추가

검색기능 추가


## 커스터마이징 부분
class BlogDetailView(DetailView):
"""
1. 이 속성은 URL에서 데이터를 찾을때 사용할 키 이름을 바꿔주는 것
보통 Django는 pk를 기준으로 데이터를 찾는데, 만약 URL 에서 id라는 이름을 쓰고 싶으면 이걸로 바꿔야함
URL이 blog/5/ 라면 pk를 쓰고, URL이 blog/<int:id>/라면 id를 씀
"""
pk_url_kwarg = 'id'
"""
2. get_queryset 메서드는 어떤 데이터를 보여줄지 결정하는 것으로 데이터전체에서 필터링하는 느낌
블로그 글이 100개 있는데, id=50이하인 글만 보여주고 싶을 때 사용
queryset = super().get_queryset() # 전체 데이터 가져오기
return queryset.filter(id__lte=50) # 그 중에서 id가 50 이하인 것만 골라내기
결과는 50번째 글까지만 보여줌
"""
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(id__lte=50)
"""
3. get_object 메서드는 URL에서 특정 글(데이터) 하나를 가져오는 방법을 바꾸는것
지금은 별로 바뀌는게 없지만, 나중에 더 복잡한 조건(ex.이 글을 작성한 사람만 볼 수 있다) 같은 걸 추가가능
object = super().get_object() # 기본 방식으로 글 하나 가져오기
object = self.model.objects.get(pk=self.kwargs.get('pk')) # 다시 글 찾기
self.model.objects.get()은 pk = 5인 데이터를 데이터베이스에서 직접 가져오는 방법
self.kwargs.get('pk') 는 URL에서 pk값을 가져옴
결과는 그냥 글 하나 가져오는 기본 방식이랑 거의 똑같지만, 나중에 수정할 준비를 해둔 것
"""
def get_object(self, queryset=None):
object = super().get_object()
object = self.model.objects.get(pk=self.kwargs.get('pk'))
return object
"""
4. 템플릿 추가로 데이터를 전달하는 것으로 템플릿에서 사용할 수 있는 변수를 더 만드는 것
템플릿에 CBV라는 텍스트를 표시하고 싶을때 쓸 수 있음
결과는 템플릿에서 {{ test }} 를 쓰면 CBV라는 값이 나옴
context = super().get_context_data(**kwargs) # 기본 데이터 가져오기
context['test'] = 'CBV' # 추가로 test라는 이름으로 'CBV' 넣기
"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['test'] = 'CBV'
return context
LoginRequiredMixin
CreateView

블로그 작성 페이지까지는 들어갔지만 생성이 안되는 오류 해결

변경가능한 pk가 필요할 경우
http://127.0.0.1:8000/cb/ 로 가짐http://127.0.0.1:8000/cb/175/ 작성한 글에 대한 상세페이지로 가짐
# 1
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.author = self.request.user
self.object.save()
return HttpResponseRedirect(self.get_success_url())
# 2
def form_valid(self, form):
blog = form.save(commit=False)
blog.author = self.request.user
blog.save()
self.object = blog
return HttpResponseRedirect(self.get_success_url())


# 1
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(author=self.request.user)
# 2
def get_object(self, queryset = None):
self.object = super().get_object(queryset)
if self.object.author != self.request.user:
raise Http404
return self.object



Admin 계정으로 로그인 한 경우 일반페이지에서도 글 관리가 가능하도록
user2 가 작성한 글을 admin이 수정이나 삭제가 불가







Chapter 06. [블로그] 댓글 기능 만들기 ⭐️


STATIC_DIR = BASE_DIR / 'static'STATICFILES_DIRS = [STATIC_DIR]STATIC_ROOT = BASE_DIR / '.static_root'
href = "../static/css/bootstrap.css" -> 정적 / {% load static %} -> import 느낌href="{% static 'css/bootstrap.css'}settings.py 에 STATIC_DIR 을 저장했기 때문에css/js 경로로 시작 가능css/bootstrap.css / js/bootstrap.js 



blog_create.html 과 blog_update.html 을 하나로 합치기blog_update.html 삭제 / blog_create.html 파일명을 blog_form.html 변경cb_views.py 와 views.py 의 template_name 도 변경




TimestampModel 은 추상 모델 클래스(abstract model class)created_at 와 updated_at 필드를 자동으로 추가하도록 하기 위한 역할python manage.py makemigrations | python manage.py migrate
TabularInline

fields = 넣은 모델에서 어떤 부분을 수정할지 wigets = 안넣으면 기본값으로 들어가는데 실제 화면에 보여줄때 어떻게 나타낼지 보여줌textinput = input창 생김(타입은 text) / attrs = input안에 특정한 값을 넣고 싶을때 
{% if request.user.is_authenticated %}
if not comment_form.is_valid(): - 유효성 검사 실패 시if not self.request.user.is_authenticated: - 로그인 여부 확인comment = comment_form.save(commit=False) comment.blog_id = self.kwargs['pk'] - 현재 블로그 게시글(pk)과 댓글을 연결comment.author = self.request.user - 현재 로그인한 사용자를 댓글 작성자로 설정comment.save() - 댓글을 데이터베이스에 저장


cb_views.py 에 CommentCreateView 를 작성하여 별도의 뷰를 통해 댓글 작성 기능을 제공하는 방식

Chapter 7. [블로그] 이미지 업로드 기능 만들기 ⭐️
설치: poetry add django-summernote


admin.py 에서 SummernoteModelAdmin 을 상속 -> Summernote 를 사용 가능

이미지가 제대로 보이도록 blog_detail.html 코드를 수정


admin 에서는 summernote 사용이 가능하지만 localhost:8000 에서는 아직 불가 forms.py 에서 BlogForm 클래스를 수정cb_views.py 에서 BlogCreateView , BlogUpdateView 클래스를 수정
settings.py 최하단에 붙여넣기 
SUMMERNOTE_CONFIG = {
# Or, you can set it to `False` to use SummernoteInplaceWidget by default - no iframe mode
# In this case, you have to load Bootstrap/jQuery sources and dependencies manually.
# Use this when you're already using Bootstrap/jQuery based themes.
'iframe': False,
# You can put custom Summernote settings
'summernote': {
# As an example, using Summernote Air-mode
'airMode': False,
# Change editor size
'width': '100%',
'height': '480',
# Use proper language setting automatically (default)
# Toolbar customization
# https://summernote.org/deep-dive/#custom-toolbar-popover
'toolbar': [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['fontname', ['fontname']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', ]],
['view', ['fullscreen', 'help']],
],
# Or, explicitly set language/locale for editor
'lang': 'ko-KR',
# You can also add custom settings for external plugins
# 'print': {
# 'stylesheetUrl': '/some_static_folder/printable.css',
# },
'codemirror': {
'mode': 'htmlmixed',
'lineNumbers': 'true',
# You have to include theme file in 'css' or 'css_for_inplace' before using it.
'theme': 'monokai',
},
},
# Require users to be authenticated for uploading attachments.
'attachment_require_authentication': True,
# You can completely disable the attachment feature.
'disable_attachment': False,
# Set to `False` to return attachment paths in relative URIs.
'attachment_absolute_uri': True,
# test_func in summernote upload view. (Allow upload images only when user passes the test)
# https://docs.djangoproject.com/en/2.2/topics/auth/default/#django.contrib.auth.mixins.UserPassesTestMixin
# You can add custom css/js for SummernoteWidget.
}
codeview 를 이용해서 블로그를 들어가면 다른 위험한 사이트로 연결되도록 하는 것을 막기위함iframe = True 로 해야 admin페이지에서 보임blog = Blog.objects.get(id= 고쳐야할 블로그 id)blog.contentblog.content = '변경할 내용 적기'blog.save()
with Pillowmodels.py 에 이미지 필드를 추가imageFieldFileField 와 같지만 이미지 파일만 업로드하도록 되어있는 VARCHAR 필드upload_to 는 미디어 폴더 안의 경로를 설정upload_to='blog/%Y/%m/%d'blog/2025/11/13/이미지파일.jpeg 처럼 나옴 Pillow 라이브러리 를 설치 필요poetry add pillow
admin 페이지에서는 확인 완료


fields = ('category', 'title', 'image', 'content') 
blog_form.html 를 이미지가 들어가도록 수정(인코딩 타입 수정 )enctype="multipart/form-data"class BlogUpdateView(LoginRequiredMixin, UpdateView):
model = Blog
template_name = 'blog_form.html'
form_class = BlogForm
.
.
.
# 추가
def form_valid(self, form):
print(form.cleaned_data)
return super().form_valid(form)
.
.
.
# blog_list.html
{% for blog in object_list %}
<div class="my-1">
{# 수정 #}
<a href="{% url 'blog:detail' blog.pk %}" class="link-primary link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover row">
{% if blog.image %}
<img src="{{ blog.image.url }}" alt="" class="col-2">
{% endif %}
<span class="col-10">
[{{ blog.get_category_display }}] {{ blog.title }} - <small>{{ blog.created_at | date:"Y-m-d" }}</small>
</span>
</a>
</div>
{% endfor %}
# # blog_detail.html
<div style="text-align: right">
{{ blog.author.username }}
</div>
<hr>
<img src="{{ blog.image.url }}" alt="" class="w-100"> {# 추가 #}

# 이미지가 제대로 들어오는지 테스트
if form.is_vaild():
print(form.cleaned_data)




blog_detail.html 에서 이미지가 있을 때만 이미지 주소를 렌더링 하도록 코드를 수정

thumbnail 필드를 추가하고 마이그레이션 파일을 만들고 마이그레이트thumbnail = models.ImageFieldnull=True, blank=True, upload_to='blog/%Y/%m/%d/thumbnail')blank=True : 홈에서 입력을 해도 되고 안해도 된다. null=True : DB에 null값이 들어갈 수 있다 
models.py 에서 form.save() 를 오버라이드하는 코드를 작성image_path = Path(self.image.name)thumbnail_name = image_path.stem thumbnail_extension = image_path.suffix


DB 설계 시에는, 중복 데이터를 최소한으로 하는 것이 가장 중요
FK 등을 적절히 활용해야 하며 텍스트보다는 숫자가 가볍기 때문에 숫자 데이터로 갖고 있는 것을 권장
숫자
Float : 부동 소수점Decimal : 고정 소수점 from config import settings
from django.conf import settings
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
from config import settingsfrom django.conf import settingssettings.DEBUG: 가 있는 이유