반응형

 

 

여름에 Django Project를 진행한 이후, 한동안 사용할 일이 없었다가 더 생각나지 않기 전에 지금까지의 장고 프로젝트 생성부터, MVT 구조, Model, View, Template을 비롯한 Single Web Application을 만드는 전반적인 내용들을 요약하여 작성하고자 한다.

 

내가 생각하기에 Django Framework의 장점은 관리자 기능이나, 로그인 등 많이 사용할 법한 인터페이스들이 Django App 모듈로 지원되기 때문에 가져다 쓸 수 있다는 것이 가장 큰 장점인 것 같다. 만약에 내가 나중에 급하게 관리자 페이지를 만들어야 하는 일이 생긴다면 장고를 택할 확률도 높을 것이라고 생각한다.

 

나중에 Django를 사용할 일이 있을 때, 해당 포스팅을 보면서 기억을 더듬을 수 있었으면 좋겠다.

 

 

 

Github : https://github.com/Jaehwi-So/LECTURE_Django_WithCloud

 

GitHub - Jaehwi-So/LECTURE_Django_WithCloud: 2023 1st Semester_ 클라우드프로그래밍 With Django & Paas-ta

2023 1st Semester_ 클라우드프로그래밍 With Django & Paas-ta - GitHub - Jaehwi-So/LECTURE_Django_WithCloud: 2023 1st Semester_ 클라우드프로그래밍 With Django & Paas-ta

github.com

 

 

 


프로젝트 구조

 

 


앱 설정 및 세팅

 

가상환경 구축 및 세팅

pip install django
django-admin startproject my_django_project

 

 

데이터베이스 초기설정

python manage.py migrate
  • 초기 db.sqlite3이 생성됨

 

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

 


앱 만들기

python manage.py startapp blog
  • settings.py에 App을 추가해주어야 함

 


데이터베이스 마이그레이션

python manage.py makemigrates
python manage.py migrate

 

 


MVT 패턴

데이터의 구조, 모양, 로직을 분리하여 개발하는 방법


 

 


Model

 

1. 모델 생성

class Post(models.Model):
    title = models.CharField(max_length=30)
    content = models.TextField()
    created_at = models.DateTimeField()

 

2. 마이그레이팅

python manage.py makemigrations
python manage.py migrate

 

3. admin.py 모델 등록

  • admin.py에 등록
admin.site.register(Post)

 

4. URL과 View 연결하기

  1. URL에 사용할 패턴과 views.py의 메서드 연결
  2. views.py에서 사용할 템플릿과 연결

 

5. 모델 정의하기

 

<models.py>

import os

from django.contrib.auth.models import User
from django.db import models
from markdownx.utils import markdown
from markdownx.models import MarkdownxField

# Create your models here.

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=200, unique=True, allow_unicode=True)   #Slug의 한글 허용
    def __str__(self):
        return self.name

    # 테이블 복수명 변경
    class Meta:
        verbose_name_plural = 'Categories'

    def get_absolute_url(self):
        return f'/blog/category/{self.slug}'

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=200, unique=True, allow_unicode=True)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return f'/blog/tags/{self.slug}'


class Post(models.Model):
    title = models.CharField(max_length=30)
    content = MarkdownxField()
    # 이미지업로드 컬럼, 경로는 _media/blog/images/년월일/에 저장
    head_image = models.ImageField(upload_to='blog/images/%Y/%m/%d/', blank=True)
    # 파일업로드 컬럼, ImageField<FileField 상위경로임
    file_upload = models.FileField(upload_to='blog/images/%Y/%m/%d/', blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # N:1 관계연결, N쪽에만 명시하면 됨
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    #author = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)

    # N:1 관계연결
    category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL)

    # N:M 관계연결
    tags = models.ManyToManyField(Tag, blank=True)   # null=True를 설정필요없음

    # 오버라이딩 : 대표 객체 속성 설정
    def __str__(self):
        return f'[{self.pk}] - {self.title}'

    # 블로그 상세보기 URL
    def get_absolute_url(self):
        return f'/blog/{self.pk}'

    # 파일 이름
    def get_file_name(self):
        return os.path.basename(self.file_upload.name)

    def get_content_markdown(self):
        return markdown(self.content)


class Comment(models.Model):
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # (작성자, 포스트) : 댓글 = 1 : N
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.author} :: {self.content}'

    def get_absolute_url(self):
        # Post View에서 해당 ID 태그의 위치로 이동
        return f'{self.post.get_absolute_url()}#comment-{self.pk}'


class Test(models.Model):
    content = models.TextField()

 

1. DateTime 자동 저장

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

 

2. Model의 대표값 설정

  • str을 오버라이딩하여 객체의 대표가 될 내용 설정

 

3. Model의 메서드 추가

  • get_absolute_url을 생성해 상세보기 페이지로 이동하는 URL 설정
  • 사용하려는 메서드를 모델에 넣으면 View에서 사용 가능

 

4. 이미지 업로드 컬럼 추가

  • settings.py와 urls.py에 MEDIA_URL 설정 필요
  • 이미지 업로드 컬럼 추가
    • head_image = models.ImageField(upload_to='blog/images/%Y/%m/%d/', blank=True)
  • 파일 업로드 컬럼 추가
    • file_upload = models.FileField(upload_to='blog/images/%Y/%m/%d/', blank=True)
  • 업로드 파일 속성
    • file_upload.name : 파일명
    • file_upload.url : 파일경로

 

5. Slug 필드 추가

  • 숫자인 pk 대신 읽을 수 있는 텍스트로 URL을 구성할 때 사용
  • models.SlugField(max_length=2000, unique=True, allow_unicode=True)

     

6. 각종 속성

  • blank=True : 해당 필드를 작성하지 않아도 됨(폼에서 비어있어도 됨), 유효성 검사때 사용
  • null=True : Nullable 허용
  • unique=True : Unique 속성
  • on_delete=models.CASCADE : 연관 레코드가 삭제될 때 함께 삭제
  • on_delete=models.SET_NULL : 연관 레코드가 삭제될 때 해당 필드를 NULL로 설정
  • allow_unicode=True : Slug 필드에서의 한글 URL 유니코드 허용

 

7. CBV 컨텍스트에 데이터 담아서 보내기

def get_context_data(self, **kwargs):
  context = super(PostList, self).get_context_data()
  context['categories'] = Category.objects.all()
  context['no_category_post_count'] = Post.objects.filter(category=None).count()
  return context

 

 

 

8. 관계 설정

    # N:1 관계연결
    category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL)

    # N:M 관계연결
    tags = models.ManyToManyField(Tag, blank=True)   # null=True를 설정필요없음

 


라우팅 설정하기

 

<urls.py>

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include('blog.urls')),
    path('markdownx/', include('markdownx.urls')),
    path('accounts/', include('allauth.urls')),
    path('', include('single_pages.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

1. URL 패턴

urlpatterns = [
    path('', views.index),
    path('<int:pk>', views.post_detail)
]

 

2. MEDIA URL(파일업로드)와 Static Path 연결하기

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

 


View

요청과 응답 처리. 모델과 템플릿을 사용자 요구에 따라 전달함

 

<views.py>

from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.shortcuts import render, redirect, get_object_or_404

from .forms import CommentForm
from .models import Post, Category, Tag
from django.views.generic import ListView, DetailView, CreateView, UpdateView

#CBV

## 포스팅 리스트
class PostList(ListView):
    model = Post
    ordering = '-pk'

    # render시 아래의 별도 설정이 없을 시 경로는 post_list.html, 모델은 자동으로 post_list로 할당됨
    template_name = 'blog/post_list.html'   #템플릿 설정

    def get_context_data(self, **kwargs):
        context = super(PostList, self).get_context_data()
        context['categories'] = Category.objects.all()
        context['no_category_count'] = Post.objects.filter(category=None).count()
        return context

## 포스팅 상세보기
class PostDetail(DetailView):
    model = Post

    def get_context_data(self, **kwargs):
        context = super(PostDetail, self).get_context_data()
        context['categories'] = Category.objects.all()
        context['no_category_count'] = Post.objects.filter(category=None).count()
        context['comment_form'] = CommentForm
        return context


## 포스팅 등록
class PostCreate(LoginRequiredMixin, CreateView):
    model = Post
    fields = ['title', 'content', 'head_image', 'file_upload', 'category', 'tags']


    def form_valid(self, form):
        current_user = self.request.user
        if current_user.is_authenticated and (current_user.is_staff or current_user.is_superuser):
            form.instance.author = current_user
            return super(PostCreate, self).form_valid(form)
        else:
            return redirect('/blog')

## 포스팅 수정
class PostUpdate(LoginRequiredMixin, UpdateView):
    model = Post
    fields = ['title', 'content', 'head_image', 'file_upload', 'category', 'tags']
    template_name = 'blog/post_form_update.html'


    def dispatch(self, request, *args, **kwargs):
        if request.user.is_authenticated and request.user == self.get_object().author:
            return super(PostUpdate, self).dispatch(request, *args, **kwargs)
        else:
            raise PermissionDenied



#FBV

## 카테고리 모아보기
def categories_page(request, slug):
    if slug=='no-category':
        category='미분류',
        post_list = Post.objects.filter(category=None)
    else:
        category = Category.objects.get(slug=slug)
        post_list = Post.objects.filter(category = category)

    context = {
        'categories': Category.objects.all(),
        'no_category_count': Post.objects.filter(category=None).count(),
        'category': category,
        'post_list': post_list
    }
    return render(
        request,
        'blog/post_list.html',
        context
    )


## 태그 모아보기
def tags_page(request, slug) :
    tag = Tag.objects.get(slug = slug)
    # 태그의 엘레멘트가 포함된 포스트를 모두 찾아야 함
    post_list = tag.post_set.all()

    context = {
        'tag': tag,
        'categories': Category.objects.all(),
        'post_list': post_list,
        'no_category_count' : Post.objects.filter(category=None).count()
    }
    return render(request, 'blog/post_list.html', context)


# 새로운 코멘트 입력
def new_comment(request, pk):
    if request.user.is_authenticated:   # 1. 인증 여부 확인
        post= get_object_or_404(Post, pk=pk)
        if request.method == 'POST':    # 2. 메서드가 POST일 경우
            # request.POST : 사용자가 폼에 입력한 데이터를 담고 있는 POST 요청 객체
            comment_form = CommentForm(request.POST)
            if comment_form.is_valid(): # 3. 폼 유효성 검사 통과시
                comment = comment_form.save(commit=False)
                comment.post = post
                comment.author = request.user
                comment.save()
                return redirect(comment.get_absolute_url())
        else:
            return redirect(post.get_absolute_url())

    else:
        raise PermissionDenied

 

1. FBV

def post_list(request):
    posts = Post.objects.all().order_by('-pk')
    return render(
        request,
        'blog/post_list.html',
        {
            'posts': posts
        }
    )
  • MYMODEL.objects.all() : Table의 모든 값 선택
  • .order_by('-pk') : PK DESC 정렬, -는 역순
  • render : 템플릿과 컨텍스트로 넘겨줄 값 설정

 

2. CBV

  • ListView, DetailView, CreateView, UpdateView
class PostList(ListView):
    model = Post
    ordering = '-pk'

    # render시 아래의 별도 설정이 없을 시 경로는 post_list.html, 모델은 자동으로 post_list로 할당됨
    template_name = 'blog/post_list.html'   #템플릿 설정

 

  • Context 추가
    def get_context_data(self, **kwargs):
        context = super(PostDetail, self).get_context_data()
        context['categories'] = Category.objects.all()
        return context

 

3. Model Query

  • SELECT ONE : Tag.objects.get(slug = slug)
  • SELECT LIST : Post.objects.all()
  • SELECT LIST WHERE : Post.objects.filter(category = category)
  • 연관되어 있는 셋 조회 : post_list = tag.post_set.all()

 

4. 인증 미들웨어

  • class PostCreate(LoginRequiredMixin, CreateView)

 

5. form_valied

  • POST 요청에서 유효한 폼이 제공되면 데이터를 확인 및 처리하여 결과 반환 
  def form_valid(self, form):
      current_user = self.request.user
      if current_user.is_authenticated and (current_user.is_staff or current_user.is_superuser):
          form.instance.author = current_user
          return super(PostCreate, self).form_valid(form)
      else:
          return redirect('/blog')

 

 

6. dispatch

  • 요청이 처리되기 전에 호출되는 메서드. HTTP 메서드에 따라 적절한 메서드 호출
  • 인증 및 권한 관리와 같은 전처리 작업을 수행함
  def dispatch(self, request, *args, **kwargs):
      if request.user.is_authenticated and request.user == self.get_object().author:
          return super(PostUpdate, self).dispatch(request, *args, **kwargs)
      else:
          raise PermissionDenied

 

7. request

  • request.method :Request Method
  • request.POST : Form에 입력한 데이터를 담고있는 POST 요청객체
  • request.user : 세션의 사용자
    • is_authenticated : 인증이 되었는가?
    • is_superadmin, is_staff : 권한이 ~인가?

 


Template

 

사용자에게 보여지는 HTML 문서의 형태를 결정한다.

 

<form.py>

 

1. 사용할 폼 클래스 생성

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('content',)   #입력

 

2. views.py에서 사용하기

    def get_context_data(self, **kwargs):
        context = super(PostDetail, self).get_context_data()
        context['comment_form'] = CommentForm
        return context

 


환경설정

 

<settings.py>

  • TIME_ZONE = 'Asia/Seoul'
  • USE_TZ = False2. Static File path 설정
  • STATIC_URL = "static/"3. 업로드 이미지 관리 및 Path 설정
  • MEDIA_URL = '/media/'
  • MEDIA_ROOT = os.path.join(BASE_DIR, '_media')

 


 

관리자 페이지 설정

 

<admin.py>

관리자 페이지 설정

 

from django.contrib import admin
from markdownx.admin import MarkdownxModelAdmin

from .models import Post, Category, Tag, Comment, Test

# Register your models here.

admin.site.register(Post, MarkdownxModelAdmin)

class AutoSlugAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('name', )}

admin.site.register(Category, AutoSlugAdmin)
admin.site.register(Tag, AutoSlugAdmin)
admin.site.register(Comment)
admin.site.register(Test)

 

1. 모델 등록

admin.site.register(Post)

 

2. 모델 생성 시 자동완성

class AutoSlugAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('name', )}

admin.site.register(Category, AutoSlugAdmin)

 


HTML 템플릿 문법

 

1. for

{% for p in post_list %}
 <p class="card-text">{{ p.content }}</p>
{% endfor %}

 

2. static path

{% load static %}
//...
<img src="{% static 'images/lena.jpg' %}">

 

3. 템플릿 필터

첫 100개 문자 <p class="card-text">{{ p.content | truncatechars:100}}</p>
첫 45개 단어 <p class="card-text">{{ p.content | truncatewords:45}}</p>

 

4. 템플릿 모듈화

 

<!-- 부모 템플릿 상속 -->
{% extends 'blog/base.html' %}

<!-- 블록 영역 구분(부모 자식 모두) -->
{% block main_area %}
{% endblock %}
<!-- 다른 템플릿 포함 -->
```html
{% include 'blog/navbar.html' %}
반응형