IT/WEB

[Django] 검색 게시판 만들기 ① : 모델(Model) 만들기

개발자 두더지 2020. 9. 10. 23:26
728x90

( 일본의 한 블로그의 게시판 만들기 튜토리얼 내용을 번역한 포스팅 시리즈입니다. )

 Django 공식 튜토리얼의 내용이 더 남아 있지만 테스트와 응용에 관련된 내용이므로 생략하고, 이번에는 로그인이나 부가적인 기능이 없으며 단순히 검색이 가능한 게시판을 만들어 보려고 한다. 완성될 페이지의 이미지는 여기에서 확인할 수 있다.

 

프로젝트 생성 및 데이터 베이스 연동


 프로젝트 생성과 데이터 베이스 연동과 관련된 자세한 내용은 이전 포스팅을 참고하길 바란다. 나는 프로젝트 이름을 'mysite2', 앱 이름을 'search'로 정의하였다. 프로젝트와 앱 생성 후 파일 구조는 아래와 같다.

 

모델 생성


1) 태그 모델

 1개의 게시글에 복수의 태그를 선택하여 글을 작성할 수 있도록 할 것이다. 먼저 태그를 표현하는 모델을 만들고자 한다. search/model.py 파일을 열어 다음과 같이 작성하자.

from django.db import models

class Tag(models.Model):
    name = models.CharField('태그명', max_length=255, unique=True)

    def __str__(self):
        return self.name

2) 게시글 모델

게시판의 주역이 될 게시글을 표현하는 모델을 만들자. 역시 search/model.py에 작성한다. 

from django.utils import timezone # 추가 import 

# class Tag 생략

class Post(models.Model):
    """기사"""
    title = models.CharField('제목', max_length=32)
    text = models.TextField('본문')
    tags = models.ManyToManyField(Tag, verbose_name='태그', blank=True)
    thumbnail = models.ImageField(blank=True, upload_to="images", null=True)

    relation_posts = models.ManyToManyField('self', verbose_name='관련기사', blank=True)
    is_public = models.BooleanField('공개', default=True)
    description = models.TextField('게시글설명', max_length=130)
    keywords = models.CharField('게시글의키워드', max_length=255, default='게시글의키워드')
    created_at = models.DateTimeField('작성일', default=timezone.now)
    updated_at = models.DateTimeField('갱신일', default=timezone.now)

    def __str__(self):
        return self.title

 thumbnail의 경우 이미지 파일이 없을 수도 있으니 null=True로 지정했다.

 relation_post는 이 게시글과 관련된 다른 게시글을 지정하기 위한 필드이다. model.MayToManyFieldself를 가리키고 있다. 관련된 게시글이므로 동일한 Post모델과 연결지어야 하니 Post로 써야한다고 생각할 수 있겠지만, self라는 점을 주의할 필요가 있다.

 is_public은 게시글 공개 여부에 대한 것이며, description은 게시글의 목록에 구글 검색 결과 처럼 게시글의 일부가 보이도록 하기 위한 것이다. keywords는 meta name="keywords"의 부분으로 현재 무언가를 저장할 용도가 아닌 대비용으로 만들어 놓은 필드이다.

 앞서 말했듯이 데이터 베이스에 저장된 기사를 보여주는 단순한 게시판을 만들 것이므로, 관리자(admin) 페이지에서 기사와 태그 등을 작성할 것이다.   

3) 댓글 모델

 게시글에 댓글 달 수 있고 확인할 수 있도록 댓글 모델을 만들고 외래키로 게시글과 연결 시킨다. 역시 동일하게 search/model.py 파일에 작성한다.

class Comment(models.Model):
    """게시글에 대한 댓글"""
    name = models.CharField('이름', max_length=255, default='이름없음')
    text = models.TextField('본문')
    target = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name='대상게시글')
    created_at = models.DateTimeField('작성일', default=timezone.now)

    def __str__(self):
        return self.text[:20]

 댓글에는 댓글 작성자와 본문, 댓글이 달린 게시글, 작성일 필드가 있다.

4) 대댓글 모델

 댓글에 대한 댓글을 달 수 있도록 만들기 위해 대댓글 모델을 만들었다. 댓글과 거의 동일하나 외래키로 Comment를 설정했다는 점이 다르다. 

class Reply(models.Model):
    """대댓글"""
    name = models.CharField('이름', max_length=255, default='이름없음')
    text = models.TextField('본문')
    target = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name='대상댓글')
    created_at = models.DateTimeField('작성일', default=timezone.now)

    def __str__(self):
        return self.text[:20]

 참고로 GenericForeignKey를 사용하면 모델을 하나로 통일할 수 있겠지만, 이번에는 직접 모델을 만들었다. 또한 Django에는 댓글에 대댓글을 재귀적으로 호출하는 것을 간단하게 할 수 있지만 여기서는 다루지 않는다.

 댓글과 대댓글 전용 페이지를 만들어 사이트에 방문한 유저가 댓글을 달 수 있도록 할 것이다.

 그리고 모델을 만들었으니  cmd창에서 프로젝트 파일로 이동한 후, python manage.py makemigration 명령어로 모델이 작성되었음을 그리고 python manage.py migrate로 스키마를 생성해주자. 이러한 모델 활성화에 대한 글은 이전 포스팅에 설명했었다.

 

미디어(사진 etc) 파일 등록을 위한 설정


위의 Post 모델의 경우 이미지를 저장하는 ImageFiled가 있었다. 이미지를 저장하고 나중에 이미지를 불러오기 위해서 미디어 파일을 다루는 설정을 할 필요가 있다.  

1) mysite2/settings.py에 media 경로 추가해주기

 settings.py 제일 아래에 설정했던 STATIC_URL와 STATIC_ROOT 아래에 다음과 같이 MEDIA_URL과 MEDIA_ROOT를 추가해줍니다.

# 위에 코드는 생략

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')

MEDIA_ROOT = os.path.join(BASE_DIR,'media')
MEDIA_URL = '/media/'

2) mysite2/urls.py에서 경로 설정해주기

settings와 static을 추가로 import해주고 url패턴 뒤에 + static~부분을 추가해준다.

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

urlpatterns = [
    path('search/',include('search.urls')),
    path('admin/', admin.site.urls),
]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

3) Pillow 라이브러리 설치

python -m pip install Pillow

Pillow 라이브러리를 설치해 동적 파일을 사용할 수 있도록 하자.

 

관리자 파일에 모델 등록


admin.py에 모델을 등록해 Django관리 사이트에서 모델들을 다룰 수 있도록 하자. search/admin.py를 열어 다음과 같이 코드를 작성하자. 

from django.contrib import admin
from .models import Post, Comment, Reply, Tag


class ReplyInline(admin.StackedInline):
    model = Reply
    extra = 5


class CommentAdmin(admin.ModelAdmin):
    inlines = [ReplyInline]


class PostAdmin(admin.ModelAdmin):
    search_fields = ('title', 'text')
    list_display = ['title', 'is_public', 'updated_at', 'created_at', 'title_len']
    list_filter = ['is_public', 'tags']
    ordering = ('-updated_at',)

    def title_len(self, obj):
        return len(obj.title)

    title_len.short_description = '제목글자수'


admin.site.register(Post, PostAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(Reply)
admin.site.register(Tag)

 이제 코드에 대해서 살펴보자. 먼저 CommentAdmin과 ReplyInline에 대해 설명하자면, 이것들은 Django관리자 사이트의 댓글의 작성, 갱신 화면에 대댓글에 대한 내용도 같이 보이도록 한 것이다. 이번에 댓글과 대댓글은 전용 페이지를 만들어 거기서 댓글을 쓰도록 할 것이지만, Django관리자 사이트에도 쉽게 다룰 수 있도하기 위함이다. 

 PostAdmin은 조금 복잡하다. search_filter는 키워드 검색을 할 때 참고되는 필드이다. list_display는 목록을 표시하고 있는 항목이다. 표시하고 싶은 모델의 필드를 작성하는 것이 일반적이지만 이 예에서는 title_len이라는 필드에는 없는 문자열을 추가하고 그것을 바로 아래에서 메소드로써 정의하여 게시글 타이틀의 문자수를 리턴하고 있다. 이것에 의해 목록에 각 게시글 제목의 문자수를 확인할 수 있도록 하였다.

 list_filter은 오른쪽에 있는 검색의 설정이며 ordering은 정렬 순서이다.

 게시글의 검색 기능은 탑 페이지에도 배치해두고 있으나 위와 같이 Django관리 사이트에도 배치해두면 편리하다. 

 그리고 보통 연결은 admin.site.register(Post)와 같이 작성하지만, admin.site.register(Post, PostAdmin)으로 한 이유는 커스터 마이즈하기 위함이다. 이에 대해서는 다음 포스팅에서 설명할 것이다. 

 관리자에 모델이 잘 등록되었는지 cmd 창에서 서버를 실행시켜 (python mange.py runserver) 관리자 페이지(127.0.0.1:포트번호/admin/)에 접속하자. 참고로 그전에 관리자 계정을 만드는 것을 잊지 말자. 관리자 계정 만드는 방법은 여기 포스팅을 참고하길 바란다. 그리고 처음 설정시에 mysite2/setting.py에 'search.apps.SearchConfig'를 작성하여 앱을 인식시켜줘야 이와 같이 관리자 화면에서 등록한 모델을 확인할 수 있다.

 이제 admin 페이지에 게시글로 출력할 데이터를 이것 저것 넣어보자!


참고자료

github.com/naritotakizawa/django-narito-blog1/blob/master/nblog1/admin.py

integer-ji.tistory.com/110

blog.narito.ninja/detail/173/

728x90