달나라 노트

Python django project 1 - 게시판 만들기 ch.14 : 게시글에 Tag 기능 추가하기 본문

Python django/Python django project 1

Python django project 1 - 게시판 만들기 ch.14 : 게시글에 Tag 기능 추가하기

CosmosProject 2020. 12. 21. 03:53
728x90
반응형

 

 

이번엔 Tag 기능을 추가해봅시다.

 

먼저 Tag 관련 model을 만들어야겠죠.

 

pro/app/tag/models.py를 아래처럼 수정합시다.

from django.db import models

# Create your models here.

class ModelTag(models.Model):
    tag_name = models.CharField(max_length=16, verbose_name='tag_name')
    registered_dttm = models.DateTimeField(auto_now_add=True, verbose_name='registered_dttm')

    class Meta:
        db_table = 'tag'
        verbose_name = 'Model_tag'
        verbose_name_plural = 'Model_tag'

    def __str__(self):
        return self.tag_name

입력받은 태그 이름을 저장하기 위한 tag_name field와 해당 tag의 등록 시간정보를 담을 registered_dttm field를 추가하였습니다.

 

 

 

pro/app/tag/admin.py를 아래처럼 수정합시다.

from django.contrib import admin
from . import models as tagapp_model

# Register your models here.

class AdminModelTag(admin.ModelAdmin):
    list_display = ('id', 'tag_name', 'registered_dttm')

admin.site.register(tagapp_model.ModelTag, AdminModelTag)

 

 

 

일단 tag app model은 완성되었습니다.

근데 잘 생각해보면 tag란 것은 각 게시물마다 태그를 입력하는 것이죠.

따라서 post app의 model에도 tag field가 필요합니다.

이를 위해서 post app models.py를 아래처럼 수정합시다.

from django.db import models

# Create your models here.

class ModelPost(models.Model):
    post_title = models.CharField(max_length=128, verbose_name='post_title')
    post_contents = models.TextField(verbose_name='post_contents')
    post_writer = models.ForeignKey('user.ModelUser', on_delete=models.CASCADE, verbose_name='post_writer')
    tags = models.ManyToManyField('tag.ModelTag', verbose_name='tags')
    registered_dttm = models.DateTimeField(auto_now_add=True, verbose_name='registered_dttm')

    class Meta:
        db_table = 'post'
        verbose_name = 'Model_post'
        verbose_name_plural = 'Model_post'

    def __str__(self):
        return self.post_title

여기서 새로운 Field가 나옵니다. ManyToManyField인데요.

이걸 Foreignkey와 비교해봅시다.

 

post app model의 post writer는 user app의 ModelUser와 ForeignKey로 연결되어 있습니다.

여기서 ForeignKey로 연결한 이유는 이 두 model의 관계가 n : 1 관계이기 때문입니다.

게시글 1개가 여러 명의 작성자를 가질 수는 없습니다. (post : user = 1 : n 은 불가합니다.)

다만 1명의 작성자가 여러 명의 게시글을 가질 수는 있습니다. (post : user = n : 1 은 가능합니다.)

이런 경우 ForeignKey를 이용하여 연결합니다.

 

하지만 Tag는 다릅니다.

게시글 1개가 여러 개의 Tag를 가질 수 있습니다. (post : tag = 1 : n 은 가능합니다.)

하나의 Tag가 여러 개의 게시글에서 사용될 수 있습니다. (post : tag = n : 1 도 가능합니다.)

이렇게 n : n의 경우에는 ManyToManyField를 사용하게됩니다.

 

 

 

여기까지 해서 model의 변경은 끝났습니다.

model의 변경이 있었으니 migration을 진행해줍시다.

 

admin 화면으로 가보면 tag model이 생성되었고,

 

 

 

post model로 진입해 ADD_MODEL_POST +  버튼을 클릭해보면 POST model에 tag field가 추가되었습니다.

 

 

 

 

 

Model은 완료되었으니 이제 Template을 봅시다.

태그를 입력받는 시점은 게시글을 등록할 때이지요.

따라서 post app tempalte의 post_register.html 파일을 봐봅시다.

 

우리가 이전에 form 태그를 이용하여 이를 구현해놨습니다.

따라서 template은 따로 손볼게 없겠군요. 왜냐면 forms.py에 존재하는 field들을 for loop를 통해서 template에 표시했으니 forms.py에 tag field를 추가해주면 됩니다.

 

 

 

 

이제 View를 손봐야하는데 그 전에 poast app의 forms.py를 수정해봅시다.

from django import forms
from app.user import models as userapp_model
from . import models as postapp_model

class FormPostRegister(forms.Form):
    post_title = forms.CharField(
        error_messages={
            'required': 'Title is required.'
        },
        max_length=128, label='Title'
    )
    post_contents = forms.CharField(
        error_messages={
            'required': 'Content is required.'
        },
        widget=forms.Textarea, label='Contents'
    )
    tags = forms.CharField(
        required=False, label='Tags'
    )

tags field를 forms.py에 추가하였습니다.

한 가지 주의할건 게시글 작성 시에 게시글 제목이나 내용은 필수 입력값이지만 태그는 사실 입력하지 않아도 되죠.

따라서 태그는 값을 입력하지 않아도 에러를 일으키지 않도록 required=False로 설정하였습니다.

 

 

 

 

 

 

여기까지 하고 보면 게시글 등록 화면에 Tag field가 추가된걸 볼 수 있습니다.

 

 

 

 

이제 post app의 views.py에 있는 view_post_register 함수를 수정해봅시다.

from django.shortcuts import render, redirect
from django.core.paginator import Paginator
from django.http import Http404
from . import models as postapp_model
from . import forms as postapp_form
from app.user import models as userapp_model
from app.tag import models as tagapp_model

# Create your views here.

...

def view_post_register(request):
    login_userkey = request.session.get('login_userkey', None)

    if not login_userkey:
        return redirect('/user/user_login/')
    else:
        if request.method == 'GET':
            formpostregister = postapp_form.FormPostRegister()

            dict_render = {
                'formpostregister': formpostregister
            }

            return render(request, 'post_register.html', dict_render)

        elif request.method == 'POST':
            formpostregister = postapp_form.FormPostRegister(request.POST)

            if formpostregister.is_valid():
                post_title = formpostregister.cleaned_data.get('post_title', None)
                post_contents = formpostregister.cleaned_data.get('post_contents', None)
                tags = formpostregister.cleaned_data.get('tags', None)

                if not(post_title and post_contents):
                    pass
                else:
                    modelpost = postapp_model.ModelPost()
                    modelpost.post_title = post_title
                    modelpost.post_contents = post_contents
                    modelpost.post_writer = userapp_model.ModelUser.objects.get(pk=login_userkey)
                    modelpost.save()

                    tag_list = tags.split(',')
                    for t in tag_list:
                        _modeltag, created = tagapp_model.ModelTag.objects.get_or_create(tag_name=t)
                        modelpost.tags.add(_modeltag)

                    return redirect('/post/post_list/')

            dict_render = {
                'formpostregister': formpostregister
            }

            return render(request, 'post_register.html', dict_render)

...

 

 

 

 

 

from django.shortcuts import render, redirect
from django.core.paginator import Paginator
from django.http import Http404
from . import models as postapp_model
from . import forms as postapp_form
from app.user import models as userapp_model
from app.tag import models as tagapp_model

# Create your views here.

...

def view_post_register(request):
	...
            if formpostregister.is_valid():
                post_title = formpostregister.cleaned_data.get('post_title', None)
                post_contents = formpostregister.cleaned_data.get('post_contents', None)
                tags = formpostregister.cleaned_data.get('tags', None)

                if not(post_title and post_contents):
                    pass
                else:
                    modelpost = postapp_model.ModelPost()
                    modelpost.post_title = post_title
                    modelpost.post_contents = post_contents
                    modelpost.post_writer = userapp_model.ModelUser.objects.get(pk=login_userkey)
                    modelpost.save()

                    tag_list = tags.split(',')
                    for t in tag_list:
                        _modeltag, created = tagapp_model.ModelTag.objects.get_or_create(tag_name=t)
                        modelpost.tags.add(_modeltag)

                    return redirect('/post/post_list/')

	...

...

다른 부분은 모두 똑같지만 위 부분을 한번 봐봅시다.

 

일단 첫 번쨰로 cleaned_data로부터 받아오는 값에 tags가 추가되어서 총 3개가 되었습니다.

 

또한 태그를 입력받을 때에는 콤마로 구분하여 입력받고, 입력받은 태그를 나눠서 체크할 것입니다.

예를들어 tag1,tag2,tag3 처럼 값을 입력하면 이를 콤마를 기준으로 나눠 list로 만든 후 해당 게시글에 tag1, tag2, tag3 3 개의 tag model을 추가할 것입니다.

 

from django.shortcuts import render, redirect
from django.core.paginator import Paginator
from django.http import Http404
from . import models as postapp_model
from . import forms as postapp_form
from app.user import models as userapp_model
from app.tag import models as tagapp_model

# Create your views here.

...

def view_post_register(request):
	...
                else:
                    modelpost = postapp_model.ModelPost()
                    modelpost.post_title = post_title
                    modelpost.post_contents = post_contents
                    modelpost.post_writer = userapp_model.ModelUser.objects.get(pk=login_userkey)
                    modelpost.save()

                    tag_list = tags.split(',')
                    for t in tag_list:
                        _modeltag, created = tagapp_model.ModelTag.objects.get_or_create(tag_name=t)
                        modelpost.tags.add(_modeltag)

                    return redirect('/post/post_list/')

	...

...

그것이 바로 위 부분입니다.

일단 ManyToManyField로 post model과 tag model을 연결시켰었습니다.

현재 다루고 있는 부분의 메인 model은 post model입니다.

즉, 메인 model인 post model에 tag model 정보를 추가해야하는데 이와 같은 경우에는 반드시 메인 model인 post model에 primary key가 존재하여야 합니다.

이 말은 사용자가 게시글 내용과 tag를 입력하여 제출하였을 때, 게시글 내용을 토대로 먼저 post model을 생성한 후, 이 생성된 post model의 primary key가 생기고 나서 post model의 tags field에 입력된 태그 정보를 추가해야 합니다.

 

따라서 위 코드를 보면 먼저 title, contents, writer 정보를 담아 post model을 save한 후 tag관련 작업을 하는 순서로 적힌 것을 볼 수 있죠.

 

tag_list = tags.split(',') : 이 부분은 입력받은 태그값을 콤마를 기준으로 나눠 list로 만드는 부분입니다.

 

 

                    for t in tag_list:
                        _modeltag, created = tagapp_model.ModelTag.objects.get_or_create(tag_name=t)
                        modelpost.tags.add(_modeltag)

중요한 것은 이 부분인데,

 

1. 태그는 중복된 값이 model에 저장되어선 안됩니다.

예를들어 이미 tag model에 tag1이라는 이름의 태그가 저장되어있습니다.

근데 어떤 유저가 tag1,tag2,tag3라는 값을 입력하여 새로운 게시글을 등록했을 때, 이는 tag1, tag2, tag3로 분리되어 다뤄질텐데 여기서 tag1은 이미 tag model에 있기 때문에 tag model에 tag1이 2개 등록되어있으면 안됩니다.

 

2. 새로 입력된 태그는 tag model에 새로 추가되어야 합니다.

어떤 유저가 tag10이라는 값을 입력했고, 이 태그 이름은 tag model에 없던 새로운 값일 때, 이 값은 tag model에 추가되어야 하는거죠.

 

위 두 사항을 조합해보면, user가 입력한 태그값이 tag model에 이미 존재하면 기존에 존재하는 tag model을 불러와서 post model에 저장하여야 하며, 기존에 존재하지 않는 tag 값을 입력했을 때에는 이 tag 이름을 tag model에 신규로 저장한 후 이를 불러와서 post model에 저장하여야 한다는 겁니다.

 

이를 가능하게 해주는 것이 바로 django model에서 objects를 불러올 때 사용할 수 있는 get_or_create method입니다.

위 내용을 보면 입력받은 tag_list를 for loop를 이용하여 tag 값을 하나씩 로직에 적용시키고 있습니다.

그리고 각 태그값은 get_or_create의 조건(tag_name=t)으로 사용되는데, get_or_create함수는 명시된 조건을 만족하는 model 있으면 해당 model을 불러와서 return 해주고, 명시된 조건을 만족하는 model의 objects가 없으면 새로운 objects를 해당 model에 추가한 후 추가된 objects를 불러옵니다.

 

또한 get_or_create의 return 값은 총 2개입니다.

이는 _modeltag와 created 2개의 변수로 get_or_create의 리턴값을 받는 형식의 코드를 보면 알 수 있는데요.

첫 번째 return 값(여기선 _modeltag)은 tag model로부터 불러와진 조건을 만족하는(또는 새로 생성된) model obejcts 내용을 담고있습니다.

두 번째 return 값(여기선 created)는 return된 값(_modeltag)이 기존에 있던 값인지, 기존에 없어서 새로 생성된 값인지 여부를 True, False의 형태로 리턴해줍니다.

 

 

 

modelpost.tags.add(_modeltag) -> 그리고 매 반복마다 반환된 tag model 정보(_modeltag)를 post app model의 tags field에 추가해주면 됩니다.

 

 

 

여기까지 하고 게시물을 등록해봅시다.

위처럼 입력하고 Submit 버튼을 눌러보면.

 

 

이렇게 새로운 글이 잘 등록되었음을 알 수 있습니다.

 

 

 

 

또한 admin의 tag model에 들어가보면 방금 입력한 tag1, tag2, tag3이 각각 등록됨을 알 수 있습니다.

 

 

 

 

그리고 admin의 model post로 가서 새로 등록된 글의 상세 내역을 보면 tag1, tag2, tag3이 추가된 것을 볼 수 있습니다.

 

 

 

 

또한 아래처럼 게시글을 하나 더 등록해봅시다.

 

 

그리고 admin에서 tag model을 보면

위 이미지 처럼 기존에 존재한 tag1, tag3는 새로 추가되지 않았고 기존에 없던 tag4만 새로 추가된 것을 알 수 있습니다.

registered_dttm을 보면 tag1, tag2, tag3는 동시에 등록되었고 tag4만 나중에 등록된 것을 알 수 있습니다.

 

 

 

 

 

 

 

이제 마지막으로 게시판 detail에서 tag까지 볼 수 있도록 수정해봅시다.

post_detail.html을 아래처럼 수정합시다.

{% extends 'base.html' %}

{% block body %}
<div class="row mt-5">
    <div class="col-12 text-center">
        <h1>Post Detail</h1>
    </div>
</div>

<div class="row mt-5">
    <div class="col-12">
        <ul class="list-group">
            <li class="list-group-item">
                <b>Title</b><br/>
                {{ post_detail.post_title }}
            </li>
            <li class="list-group-item">
                <b>Contents</b><br/>
                {{ post_detail.post_contents }}
            </li>
            <li class="list-group-item">
                <b>Tags</b><br/>
                {{ post_detail.tags.all }}
            </li>
            <li class="list-group-item">
                <b>Writer</b><br/>
                {{ post_detail.post_writer }}
            </li>
            <li class="list-group-item">
                <b>Registered at</b>: {{ post_detail.registered_dttm }}
            </li>
        </ul>
    </div>
</div>

<div class="row mt-5">
    <div class="col-6">
        <button type="button" class="btn btn-primary btn-block" onclick="location.href='/post/post_list/'">Post List</button>
    </div>
    <div class="col-6">
        <button type="button" class="btn btn-primary btn-block" onclick="location.href='/post/post_register/'">Post Register</button>
    </div>
</div>
{% endblock %}

보면 Tags 부분이 추가되었습니다.

post model에서 원하는 게시글의 primary key를 이용하여 해당 게시글의 model objects를 불러왔고, 이는 post_detail에 담겨져 위 템플릿에 전송되는 그런 흐름이었습니다.

post model에 tags field가 추가되었으니 불러와지는 model 내용에 tags도 포함되어있을 것입니다.

따라서 {{ post_detail.tags.all }} 처럼 tags field의 내용을 불러왔습니다.

근데 뒤에 all이라는 내용이 붙었습니다.

이게 무엇이냐면 post model과 tag model은 ManyToManyField로 연결되어있기 때문에 하나의 게시글이 여러 개의 tag model object를 가질 수 있습니다.

따라서 어떤 게시글이 연결된 모든 tag model 정보를 불러오게 하기 위해서 all을 넣었습니다.

 

 

 

여기까지 하고 글을 보면 

위처럼 QuerySet에 해당 게시글의 Tag정보가 표시됩니다.

Tag가 잘 불러와지는 것을 알 수 있죠.

사실 표시만 위처럼 복잡하게 되는것이고 각각의 Tag name이 ['tag1', 'tag2', 'tag3'] 처럼 list의 형태로 반환된다고 생각하시면 됩니다.

 

 

 

 

{% extends 'base.html' %}

{% block body %}
<div class="row mt-5">
    <div class="col-12 text-center">
        <h1>Post Detail</h1>
    </div>
</div>

<div class="row mt-5">
    <div class="col-12">
        <ul class="list-group">
            <li class="list-group-item">
                <b>Title</b><br/>
                {{ post_detail.post_title }}
            </li>
            <li class="list-group-item">
                <b>Contents</b><br/>
                {{ post_detail.post_contents }}
            </li>
            <li class="list-group-item">
                <b>Tags</b><br/>
                {{ post_detail.tags.all|join:', ' }}
            </li>
            <li class="list-group-item">
                <b>Writer</b><br/>
                {{ post_detail.post_writer }}
            </li>
            <li class="list-group-item">
                <b>Registered at</b>: {{ post_detail.registered_dttm }}
            </li>
        </ul>
    </div>
</div>

<div class="row mt-5">
    <div class="col-6">
        <button type="button" class="btn btn-primary btn-block" onclick="location.href='/post/post_list/'">Post List</button>
    </div>
    <div class="col-6">
        <button type="button" class="btn btn-primary btn-block" onclick="location.href='/post/post_register/'">Post Register</button>
    </div>
</div>
{% endblock %}

list에 존재하는 값을 연결할 때에는 join 함수를 사용할 수 있죠.

django에선 이렇게 python의 함수를 template에도 적용시킬 수 있습니다.

위 내용을 보면 {{ post_detail.tags.all|join:', ' }} 이렇게 적혀있는데 반환받은 list 형태로 묶인 값들에 join 함수를 적용해서 콤마를 기준으로 하나로 합쳐 표시하겠다는 뜻입니다.

 

 

여기까지 하고 다시 게시글 디테일을 보면

태그가 보기좋게 잘 표시되고있죠.

 

 

 

728x90
반응형
Comments