Django - 강의 실습

김기훈·2025년 11월 9일

Django

목록 보기
4/17
post-thumbnail

시작 준비 ✅


가상환경 설정

  • 가상환경 설정

    • pyenv virtualenv 3.12.1 oz_blog | 새로운 가상 환경 생성

      • pyenv local oz_blog | oz_blog 가상환경을 활성화

  • poetry 기반 프로젝트 시작 준비

    • poetry init : poetry 설정
    • poetry add django : django 설치
      • poetry add django == 5.0.4 : 버전 고정 설치
      • poetry add django ~= 5.0 : 5.0버전에서 최신버전 설치

Django 설정

  • django-admin startproject config .

    • django 설정파일 생성 / . 은 현재 경로에서 파일을 만들겠다는 의미
  • 인터프리터 설정


Shell

  • poetry add ipython / python manage.py shell

    • ipython — 더 똑똑한 파이썬 쉘
    • 기본 Python shell 보다 기능이 훨씬 많아서 장고 shell에서 테스트할 때 매우 편함
  • poetry django-extensions / python manage.py shell_plus

    • Django 개발 보조 확장: Django 개발 시 편리한 명령어들을 추가해줘서 생산성이 올라감
      • 자동 import shell
      • 모든 models 자동 불러오기 (매번 from app.models import User 안 해도 됨)
    • python manage.py show_urls | URL 목록 보기
    • python manage.py graph_models -a -o erd.png
      • 모델 구조 그림으로 만들기 = ERD 이미지 생성

마이그레이션

  • python manage.py makemigrations / python manage.py migrate

    • 마이그레이션 파일 생성(git 의 commit 느낌) / db에 적용 (git 의 push 느낌)
      • Django 프로젝트에서 필요한 데이터베이스 테이블을 생성하고, 데이터베이스 구조를 초기화
      • runserver 했을때 뜨는 빨간색 경고 없어짐

Admin

  • python manage.py createsuperuser | 관리자 계정 생성

    • 장고는 /admin/ 페이지를 기본 제공, 하지만 일반 유저 계정으로는 접근 불가.
    • 슈퍼유저(superuser) 라는 특별한 권한을 가진 계정만 로그인 가능.
  • 어드민 페이지 언어 변경

  • choice 기능 이용

  • def __str__(self)

    • 원래 제목이 기본적인 영어 제목이 되어야 하지만 제목의 노출형식을 설정
  • get_category_display()

    • get_컬럼명(choices를 만든)_display()
      • 원래는 [free] 가 나와야 하지만 [자유] 가 나오도록 변경
      • 'choices=' 때문에 CharField 임에도 선택박스가 나오는 것

실습 - 1 ✅

Blog 📖

Blog app 생성

  • python manage.py startapp blog

    • app을 생성한 후에 settingsINSTALLED_APPS 에 등록


1. 목록 페이지 만들기

  • 생성날짜 출력 변환


2. 상세 페이지 만들기

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

  • 제목을 눌렀을때 내용이 있는 페이지로 넘어가도록 변환


3. 상세 페이지 만들기 - 동적

  • <a href="{% url 'blog_detail' blog.pk %}">

    • url이라는 템플릿 함수가 django urls.py에서 이름이 blog_detail 을 가진 path를 찾음
      • 이 이름에 맞춰서 url경로를 맞춰줌
    • blog.pk

      • 꼭 이름이 pk가 아니라 id여도 가능, 이름으로 받는게 아니라 위치인자로 받음
      • path('blog/<int:pk>/', views.blog_detail,name='blog_detail')
        • 여기서 첫번째 인자(<int:pk>) 로 들어오는 숫자를 그냥 blog.pk 가 받는것
  • 추가로 받는 값이 없기 때문에 <a href="{% url 'blog_list' %}">


쿠키와 세션 🍪

  • 사용자 식별, 행동 정보 기억을 위해 필요
  • 매번의 요청은 독립적인 사건이기 때문에 기억해야할 정보를 함께 요청하는 작업이 필요

쿠키

  • 유저단에 정보 저장 (클라이언트)
  • 텍스트
  • 브라우저에 저장되기 때문에 외부 유출에 취약
  • 보안등급이 높은 정보는 쿠키에 저장할 수 없음
  • 종료 시점을 설정할 수 있고, 미설정 시 브라우저 종료와 동시에 쿠키도 소멸됨
  • 한 도메인당 20개, 쿠키 하나당 4KB로 총 300개의 용량 제한이 있음

세션

  • 보안상 브라우저에서 갖고 있을 수 없는 중요 정보를 저장할 수 있다.
  • 서버에 저장됨
  • 서버는 쿠키가 가진 key와 매핑된 value를 딕셔너리 형태로 저장. (Object형)
  • 브라우저가 쿠키의 key로 요청을 하게되면 서버에서는 key를 가지고 유저의 세션 정보를 파악할 수 있음
  • 정확한 소멸 시점을 알 수 없음
  • 서버가 허용하는 선에서 용량 제한이 없음

실습

  • visits = int(request.COOKIES.get('visits',0)) +1

    • get
      • visits값을 key값으로 가져오고 Key값이 None일 경우에 디폴트로 지정해준 0을 사용
    • return 되는 값이 str이기 때문에 int로 변환

LOGIN ✅

로그인 페이지

  • Django Authentication 공식문서

    • 로그인 정보는 보안이 중요하기 때문에 세션에 저장

      • Django에 내장되어있는 Django Authentication을 이용하여 실습 예정
  • accounts 의 기능들 확인

    • admin 페이지에서 만든 superuser로 로그인 함
    • 로그인 url연결

로그아웃 페이지


csrf

  • 클라이언트와 서버가 공유하는 인증값이 담긴 보안용 토큰

    • settings.py
      • MIDDLEWARE 에 설정되어있어 모든 Post에서 CSRF 토큰을 검증
      • crsf 토큰을 주지 않으면 검증에 실패하여 Post 요청이 들어가지 않음
      • username을 보여주는 코드를 추가하여 로그인이 잘 되었는지 확인

회원가입 페이지

    1. app는 모듈화가 가능하도록 만들어야 하기 때문에 회원가입 관련 App 생성
    • python manage.py startapp member
    • UserCreationForm
    1. 비밀번호 정책이 이미 settings 에 존재하고,
    • form = UserCreationForm(request.POST)
      • Formrequest.POST 를 넣어주면 자동으로 validation

    1. 정책을 통과하면 로그인창으로 넘어가고 회원가입한 아이디로 로그인도 가능
    1. 회원가입 버튼 만들기

추가 ✅

코드 단순화

  • form = UserCreationForm(request.POST or None)
    • post 일 경우 post데이터가 들어가고 아닐경우 none데이터가 들어감

settings 임포트

  • from django.conf import settings (권장)
    • 장고가 실행되고 있는 환경에서 settings파일을 가져옴
    • 만약 config폴더의 이름이 변경되더라도 알아서 찾아서 가져옴
  • from config import settings
    • 폴더에 있는 그대로 가져온 것

추가 실습

Login 기능 직접 만들기

  • redirect(reverse('blog_list'))
    • from django.urls import reverse
      • reverse : 이름을 가지고 어떤 url로 갈지 찾아줌
      • config/urls.py 에서 blog_list 라는 이름을 가진 url을 찾고 redirect

실습 - 2 ✅

상세페이지

작성자 컬럼

  • 블로그에 작성자 컬럼 만들기

  • author = models.ForeignKey(User, on_delete=models.CASCADE)
    • models.CASCADE -> 같이 삭제
    • models.PROTECT -> 삭제가 불가능함
      • 유저를 삭제하려고할때 블로그가 있으면 유저 삭제가 불가능
    • models.SET_NULL -> null값을 넣음
      • 유저 삭제시 블로그의 author가 null이 됨,
      • 이 때 null=True 옵션도 함께 설정 필요

작성자 이름 출력


Base.html로 템플릿 extends

  • Template Engine extends
    • 전체페이지, 네비게이션 같은 공통된 템플릿을 공유
    • {% block content %}{% endblock %}
      • 공통적 으로 갖는 상단 네비바를 제외하고 자유롭게 변형할 수 있도록 설정


추가 이론

User

  • from django.contrib.auth.models import User
  • from django.contrib.auth import get_user_model
  • User = get_user_model()

    • 항상 django.contrib.auth.models의 User을 사용은 X
      • get_user_model() 사용
      • User을 새롭게 만들어야 할 경우 연결된 User이 어떤것인지 파악 후 가져옴
      • 즉, 장고에 설정되있는 유저를 찾아서 가져오는 함수

모델의 수정사항이 발생

  • python manage.py makemigrations / python manage.py migrate

블로그 Form

  • Django Forms의 기능
      1. HTML에 input 그려주기
      1. validation (검증)
      1. 저장 및 업데이트

  • Form 실습


forms.py

from django import forms
from blog.models import Blog

class BlogForm(forms.ModelForm):
    class Meta:
        model = Blog
        fields = ('title', 'content', )
  • class BlogForm(forms.ModelForm):
    • django의 forms를 가져와서 그 forms안에 있는 ModelForm을 상속받음
  • fields
    • 어떤 컬럼을 forms에 적용할지 지정
      • 전체 적용: __all__ / 특정 컬럼: list , dict

views.py

from 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)
    • BlogForm
      • Django의 ModelForm 을 기반으로 한 폼 클래스
    • 사용자가 POST 요청(글 작성 폼 제출)을 하면 request.POST 데이터를 이용해 폼을 채움
    • GET 요청(페이지 처음 접속)은 request.POST가 없으므로 None이 들어가 빈 폼이 표시
  • if form.is_valid():
    • 사용자가 입력한 데이터가 폼의 유효성 검사(예: 빈칸, 글자 수 제한 등) 를 통과했는지 확인
    • True면 아래 저장 로직이 실행, 아니면 마지막의 return render(...)로 다시 폼을 보여줌
  • blog = form.save(commit=False)
    • 폼의 데이터를 기반으로 Blog 모델 인스턴스를 만들지만 DB에는 아직 저장하지 않음을 의미
    • commit=False 덕분에 DB 저장 전에 추가로 author를 지정할 수 있게 됨
  • blog.author = request.user
    • 현재 로그인한 사용자를 author 필드에 연결
    • 즉, 새로 작성된 글의 작성자를 현재 로그인한 사용자로 지정
  • blog.save()
    • 실제로 DB에 저장, 이제 새 블로그 글이 생성되고 blog.pk(기본키, ID) 가 생김
  • return redirect(reverse('blog_detail', kwargs={'pk': blog.pk}))
    • 저장 후, 새로 작성된 글의 상세 페이지로 리다이렉트
    • reverse('blog_detail', kwargs={'pk': blog.pk})
      • 'blog_detail'이라는 URL 패턴에 pk 값을 넣어서 URL 문자열을 만들어 줌(ex. /blog/5/)
context = {'form': form}
return render(request, 'blog_create.html', context)
  • 폼이 유효하지 않거나 처음 접속한 경우,
    • blog_create.html 템플릿을 렌더링해서 사용자에게 폼을 보여줌
  • contextform 을 담아 템플릿에서 {{ form.as_p }}처럼 사용 가능

  • Form 실습 2

    • 로그인 하지 않은 유저가 작성 할 경우

      • 로그인 페이지로 보내버리기


@login_required()

  • 로그인 페이지를 지정해주지 않아도 자동으로 지정됨
    • settings의 LOGIN_URL = '/accounts/login/'
      • http://127.0.0.1:8000/accounts/login/?next=/create/
      • 다음페이지가 어딘지도 나옴 next=/create/

  • 로그인 해도 next뒤에 있는 페이지로 안가짐

# 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.author

    • 이 조건 없으면 아무한테나 다 수정버튼이 보이게 됨
  • instance=blog
    • 블로그 인스턴스를 넣어주면 폼의 항목에 맞게 타이틀,컨텐츠 등등 구분이 자동으로 들어감

페이지네이션

  • 준비: 이미 있던 글을 여러번 반복해서 만듬

10개씩 나누기

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

페이지번호 링크를 추가

특정 페이지로 이동 추가

  • page_object.number : 현재 페이지

    1. 각 페이지 번호에 대한 링크를 출력, 현재 페이지는 (현재페이지)로 표시
    • for i in page_object.paginator.page_range
      • 0 에서 최대페이지 까지 range에 들어가 있음
    • elif i > page_object.number|add:-3 and i < page_object.number|add:3
      • 현재 페이지 -3 보다 크고 현재 페이지 +3 보다 큰
        • ex. 현재 페이지: 2 => -1 ~ 5
    1. 현재 페이지 번호 근처의 페이지 번호 링크를 출력(현재 페이지 -2 > 1보다 크면)
    • ... 누르면 현재페이지 + 3 페이지로 가짐
    • <a href="?page={{ page_object.number|add:-3 }}">&hellip;</a>
    1. 마지막 페이지로 이동하는 링크와 현재 페이지 기준으로 앞뒤의 ... 표시를 추가
    • (최대페이지 > 현재페이지 + 2)
    • <a href="?page={{ page_object.number|add:3 }}">&hellip;</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'])
    • 특정 요청만 허락하는 데코레이터. 삭제나 수정은 POST 요청으로 받아야함
    • 삭제 요청은 꼭 POST요청으로 받아야 함 / GET는 안됨
      • POST요청 = csrf_token 필요


실습 - 3 ✅


CBV. 상세페이지 만들기

CBV(Class Based View)

| Class-Based Views 공식문서 | generic view |

  • Python 클래스로 뷰를 정의하는 방식
    • CBV는 Django의 뷰에 대한 다양한 기능을 더 구조적으로 사용할 수 있게 해줌
  • 장점
    • 재사용성: CBV는 상속과 믹스인(Mixins)을 통해 코드의 재사용성을 높일 수 있음
    • 구조화된 코드: 뷰 로직을 클래스 메서드로 나누어 복잡한 뷰를 더 구조적으로 관리할 수 있음
      • 잘 쓸경우 코드가 짧아짐 / 구조가 나누어져 있어서 코드 가독성 증가
    • 확장성: 다양한 클래스 기반 뷰
      • (ex. ListViewDetailViewCreateView)를 사용해 일반적인 패턴을 쉽게 구현
  • 단점
    • 배워야 하는게 많아짐
    • 잘 모르고 사용할 경우 너무 어렵고 복잡

TemplateView

  • 지금까지 했던 방법

  • 한줄로 처리 가능

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

  • 어바웃 페이지로 이동하는 리다이렉트 뷰(기본 방법 / 익명함수 lambda를 사용하는 방법)

  • path('redirect/', RedirectView.as_view(pattern_name='about'), name='redirect')

    • http://127.0.0.1:8000/redirect/ pattern_name과 동일 이름으로 이동
    • redirect / redirecr2 둘다 about 로 이동함

TestView

  • 이전까지는 POST, GET 등 호출 유형 별로 if~else문을 써야했음
    • 지금부터는 CBV를 활용해 함수만으로 구현이 가능

블로그 List 페이지를 CBV로 바꾸기

# 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
  • model = Blog 이렇게 넣으면 무조건 Blog.objects.all() 로 가져옴

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

    • queryset = Blog.objects.all().order_by('-created_at') 이렇게 안하고
    • 따로 queryset = Blog.objects.all() / ordering = ('-created_at') 가능
  • 검색기능 추가


블로그 Detail 페이지를 CBV로 바꾸기

  • pk를 기준으로 int값을 가져오기 때문에 urls에서 pk로 잡아야 함
  • object.content로 해도 cbv에서는 작동 하지만 fbv에서는 작동 X
## 커스터마이징 부분

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

블로그 Create 페이지를 CBV로 바꾸기

  • LoginRequiredMixin

    • FBV에서는 @login_required 사용
    • 사용자가 로그인을 해야만 이 뷰에 접근할 수 있도록 함
    • 만약 로그인을 하지 않은 사용자가 접근하려고 하면, 로그인 페이지로 리디렉션
  • CreateView

    • Django에서 제공하는 제네릭 뷰로, 새로운 객체를 생성하는 폼을 처리하는 데 사용
  • 블로그 작성 페이지까지는 들어갔지만 생성이 안되는 오류 해결

  • 변경가능한 pk가 필요할 경우

    • 앞선 코드는 블로그를 작성하고 생성하면 바로 http://127.0.0.1:8000/cb/ 로 가짐
      • 바로 리스트로 간다건다 하는 정적인 url이 필요하면 success_url = 사용
    • 함수를 추가한 코드는 http://127.0.0.1:8000/cb/175/ 작성한 글에 대한 상세페이지로 가짐
      - 디테일 페이지로 가야해서 디테일 pk가 필요하다면 def get_success_url(self) 사용

추가

# 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())
        
  • 둘다 가능

블로그 Update 페이지를 CBV로 바꾸기

  • BlogUpdateView 클래스의 get_success_url 함수 대신
    • 모델에 get_absolute_url 함수를 사용하여 객체의 고유 URL을 반환
  • models.py에 이렇게 해놓으면 cb_views.py에서 success_url이 없을경우 자동으로
    • models.py의 get_absolute_url을 찾아서 이용 함
      • create에서도 success_url 안해도 됨

  • author=self.request.user
    • 본인이 작성한 글이 아닐 경우 404 반환

추가

  • 유저가 같은 유저가 맞는지 확인하는 코드
# 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
  • 두가지 방법 전부 가능

블로그 Delete 페이지를 CBV로 바꾸기

삭제 버튼을 눌렀을 때, 팝업창으로 한 번 더 확인하도록


URL include

  • 공통된 url주소를 통해 관리를 용이하게 할 수 있음(ex. product/ , product/123 )
    • blog에서 urls.py 파일을 생성하고 blog와 관련된 url을 관리
      • fbv_urls.py 파일도 생성해서 관리

실습

  • include 전 {% url 'blog_list' %} / include 후 {% url 'blog:list' %}
    • blog 는 app_name

Admin 권한 처리

  • Admin 계정으로 로그인 한 경우 일반페이지에서도 글 관리가 가능하도록

  • user2 가 작성한 글을 admin이 수정이나 삭제가 불가


카테고리

  • 카테고리가 안잡힌 것들도 있어서 업데이트 해보기
    • null이 아니라 공백으로 잡혀있어서 isnull=True 안통한 것

  • 수정 및 업데이트에 카테고리 추가

실습 - 4 ✅

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


Bootstrap

  • 자주 사용하는 js 와 css 가 모여있는 프레임워크

    • 다운로드 받은 파일을 프로젝트 루트위치에 static/ 폴더를 만들어서 그안에 넣음

  • Django Static File

    • CSS, JavaScript, 이미지 파일 등과 같은 정적 자산을 관리하는 데 사용
    • 이러한 파일들은 서버 측에서 동적으로 생성되지 않으며, 클라이언트에 그대로 제공

  • STATIC_DIR = BASE_DIR / 'static'

    • 그냥 정의하기 위해 사용한 것
  • STATICFILES_DIRS = [STATIC_DIR]

    • 정적 파일들을 모아 관리하기 위해 만든 리스트
  • STATIC_ROOT = BASE_DIR / '.static_root'

    • 배포 시 한 번에 배포하기 위함
      • 배포환경에서 널려져 있는 스태틱 파일들을 한곳으로 모아 관리 가능하게 한 것

base.html

  • href = "../static/css/bootstrap.css" -> 정적 / {% load static %} -> import 느낌
    • href="{% static 'css/bootstrap.css'}
  • settings.pySTATIC_DIR 을 저장했기 때문에

    • static 디렉토리 안에있는 css/js 경로로 시작 가능
    • css/bootstrap.css / js/bootstrap.js

Bootstrap 적용하기


  • blog_create.htmlblog_update.html 을 하나로 합치기
    • blog_update.html 삭제 / blog_create.html 파일명을 blog_form.html 변경
    • cb_views.pyviews.pytemplate_name 도 변경
      • 작성과 수정 모두 블로그 작성이 나옴

  • 위 처럼 해놓으면 FBV에서는 안나옴
    • 변형
      • btn_name 있으면 btn_name 출력 없으면 '저장' 텍스트 나오도록


댓글 기능


댓글 모델 만들기

  • TimestampModel 은 추상 모델 클래스(abstract model class)

    • 이 클래스를 다른 모델 클래스가 상속 받아서 
    • created_at 와 updated_at 필드를 자동으로 추가하도록 하기 위한 역할
  • model이 추가되었기 때문에 사용하여 반영

    • python manage.py makemigrations | python manage.py migrate

  • admin 페이지에서 테스트

  • admin에서 댓글관리를 할 수 있도록 설정
    • TabularInline
      • Django의 관리자(admin) 인터페이스에서 사용되는 클래스 중 하나
      • 외래 키로 연결된 모델들을 한 줄씩 표 형태로 인라인(inline)으로 표시해주는 역할
      • 이를 통해 관리자는 주 모델의 변경 화면에서 바로 관련된 객체들을 관리 가능
        • 현재 코드에서는 블로그와 관련된 댓글을 하나의 화면에서 편리하게 관리 가능

댓글 작성

  • fields = 넣은 모델에서 어떤 부분을 수정할지
  • wigets = 안넣으면 기본값으로 들어가는데 실제 화면에 보여줄때 어떻게 나타낼지 보여줌
    • textinput = input창 생김(타입은 text) / attrs = input안에 특정한 값을 넣고 싶을때

  • {% if request.user.is_authenticated %}

    • 로그인 되어 있는지 체크
      • 로그인이 되어있지 않다면 댓글을 작성할 수 없도록

  • 작성은 되는데 어드민페이지에서만 작성한 댓글이 보임
      1. if not comment_form.is_valid(): - 유효성 검사 실패 시
      1. if not self.request.user.is_authenticated: - 로그인 여부 확인
      1. comment = comment_form.save(commit=False)
      • 댓글 객체를 데이터베이스에 저장하지 않고 반환
      1. comment.blog_id = self.kwargs['pk'] - 현재 블로그 게시글(pk)과 댓글을 연결
      1. comment.author = self.request.user - 현재 로그인한 사용자를 댓글 작성자로 설정
      1. comment.save() - 댓글을 데이터베이스에 저장


  • 댓글이 나중에 적은게 아래로 감 = 댓글 역정렬 필요

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

페이지네이션

  • 상세 페이지에 댓글 페이지별로 보여주기


실습 - 5 ✅


Chapter 7. [블로그] 이미지 업로드 기능 만들기 ⭐️


이미지 업로드 기능


summernote

  • 설치: poetry add django-summernote

  • 기본 설정


  • 주의 ⚠️

    • summernote에 migrate 해야하는 파일이 존재하기 때문에 한번 migrate 해야함

  • 기능 추가

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

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

    • 파일 선택으로 이미지를 업로드하면 media 디렉토리 에 들어감

  • 블로그 Form에 Summernote 붙이기

    • admin 에서는 summernote 사용이 가능하지만 localhost:8000 에서는 아직 불가
      • forms.py 에서 BlogForm 클래스를 수정
  • cb_views.py 에서 BlogCreateView , BlogUpdateView 클래스를 수정

    • OPTIONS 코드를 settings.py 최하단에 붙여넣기

  • summernote option (불필요한 부분 제거 )

    • option을 추가해서 사이트에 맞게 사이즈를 정해놓을 수 있음
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.
}

toolbar


리치 텍스트 편집기(RTE)

  • Rich Text Editor

    • 사용자가 문서의 텍스트를 서식화하고 스타일링할 수 있도록 하는 도구
      • 일반적으로 HTML 및 CSS를 기반으로 하며, 다양한 서식 옵션과 기능을 제공
      • 사용자가 시각적으로 매력적인 콘텐츠를 작성할 수 있도록 돕는다.

  • Rich Text Editor를 통해 들어오는 위험한 접근 막기

    • js calculator
    • codeview 를 이용해서 블로그를 들어가면 다른 위험한 사이트로 연결되도록 하는 것을 막기위함
    • iframe = True 로 해야 admin페이지에서 보임
      • 블로그 들어가면 바로 구글로 가버리기 때문에 망가져 버렸음

  • 실습

    • blog = Blog.objects.get(id= 고쳐야할 블로그 id)
    • blog.content
      • id에 맞는 블로그 글이 나옴
      • blog.content = '변경할 내용 적기'
    • blog.save()
      • 변경할 내용으로 id에 맞는 블로그의 내용이 변경됨
  • 주의 ⚠️

    • codeview 는 막아두는게 좋음
  • 유용한 라이브러리

  • bleach


블로그에 이미지 업로드하기

  • with Pillow

    • models.py 에 이미지 필드를 추가
      • imageField
        • FileField 와 같지만 이미지 파일만 업로드하도록 되어있는 VARCHAR 필드
        • 여기에는 이미지의 주소(경로)가 들어감
      • upload_to 는 미디어 폴더 안의 경로를 설정
        • upload_to='blog/%Y/%m/%d'
        • blog/2025/11/13/이미지파일.jpeg 처럼 나옴
    • Image를 사용하기 위한 Pillow 라이브러리 를 설치 필요
      • poetry add pillow
    • 모델을 수정했기때문에 마이그레이션 파일을 만들고 마이그레이트
  • admin 페이지에서는 확인 완료


  • 홈에서 이미지 업로드 실습

  • 1. 어드민에서는 가능했는데 아직 이미지 업로드가 안됨

    • BlogForm 에 이미지 필드를 추가
      • fields = ('category', 'title', 'image', 'content')

  • 2. 이미지 업로드는 가능하지만 아직 올라간 사진이 보이지는 않음

    • 수정 페이지에서는 이름만
      • blog_form.html 를 이미지가 들어가도록 수정(인코딩 타입 수정 )
      • enctype="multipart/form-data"
      • django 는 같은 이름의 사진을 업로드하면, 무작위 난수로 이름을 변경시킨다.
  • 3. 이미지가 잘 들어가는지 폼 검증 함수 추가

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)
	.
    .
    .
  • 4. 업로드한 이미지가 리스트와 상세페이지에서 보이게 하기

# 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"> {# 추가 #}

FBV

CBV에서 FBV로 잠시 전환


  • FBV 이미지 업로드 테스트

    • CBV에서는 이미지업로드가 가능햇는데 FBV에서는 작동 안되는 중
# 이미지가 제대로 들어오는지 테스트 
if form.is_vaild():
	print(form.cleaned_data)


  • 이미지 띄우기

    • list / detail
      • img src="{{ blog.image.url }}"
        • image = models.py 의 ImageField 자체

  • detail
    • class="w-100" 보여지는 이미지 크기 조절

오류

  • 코드 작성 후 이미지가 없는 블로그에 들어가면 오류발생

    • 이미지가 없는 글의 상세페이지로 이동할 때 발생하는 오류를 해결
      - blog_detail.html 에서 이미지가 있을 때만 이미지 주소를 렌더링 하도록 코드를 수정

Thumbnail

  • 이미지가 포함된 게시글은 목록페이지에서 제목 옆에 이미지가 보이도록 만들기
    • thum은 용량이 작아서 일반 사진보다 훨씬 빠르게 로드 가능

  • 1. thumbnail 필드를 추가하고 마이그레이션 파일을 만들고 마이그레이트

    • thumbnail = models.ImageField
    • ('썸네일', null=True, blank=True, upload_to='blog/%Y/%m/%d/thumbnail')
      • blank=True : 홈에서 입력을 해도 되고 안해도 된다.
      • null=True : DB에 null값이 들어갈 수 있다

  • 2. models.py 에서 form.save() 를 오버라이드하는 코드를 작성

    • 오버라이드
      • 상속 관계에서 부모 클래스가 가진 메서드를 자식 클래스가
      • “같은 이름으로 다시 정의해서 덮어쓰는 것”
      • 요약: 부모가 준 기능이 있는데, 자식이 그 기능을 자기 방식대로 고쳐 쓰는 것
    • image_path = Path(self.image.name)
      • Path 라이브러리를 사용해 이미지 경로를 가져옴
    • thumbnail_name = image_path.stem
      • path 라이브러리를 통해 stem을 가져오면 마지막 확장자를 제외한 이름을 가져옴
      • /blog/2024/8/13/database.png -> database
        -thumbnail_extension = image_path.suffix
      • .을 포함한 확장자를 가져옴
      • /blog/2024/8/13/database.png -> .png

  • 3. blog_list.html에 썸네일을 적용


    1. Blog 모델을 이용해 썸네일 이미지 주소를 가져오는 방법도 가능

Django의 Field들과 DB 설계를 잘 하는 법

  • DB 설계 시에는, 중복 데이터를 최소한으로 하는 것이 가장 중요

  • FK 등을 적절히 활용해야 하며 텍스트보다는 숫자가 가볍기 때문에 숫자 데이터로 갖고 있는 것을 권장

  • 숫자

    • Float : 부동 소수점
      • 조금 부정확하지만 큰 수
    • Decimal : 고정 소수점
      • 정확하지만 상대적으로 작은 수
        • Decimal('1.1') 이런식으로 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 settings
  • from django.conf import settings
    • 두개 다 가능하지만 django.conf 를 사용하면 settings파일이 바뀌거나 경로가 변경되도
      • 경로가 실행된 환경에 있는 settings를 가져오기 때문 = 훨씬 더 안전
  • settings.DEBUG: 가 있는 이유
    • 실제로 배포환경에서는 경로가 변경될것이기 때문에

profile
안녕하세요.

0개의 댓글