Django web project 개발일지6) 포토앱 기능 추가

Mongle·2020년 7월 29일
0

DjangoBoard

목록 보기
6/7
post-thumbnail

📷 Pillow 라이브러리를 활용해서 커스텀 필스를 작성하고 사진을 업로드하는 기능을 중점으로 만들어볼 것이다. 기본 뼈대를 구성하고 업로드, 수정, 삭제 등 기능을 추가해보자.

먼저, photo앱을 등록하고 시작하자

INSTALLED_APPS = [
    'bookmark.apps.BookmarkConfig',
    'blog.apps.BlogConfig',
    'taggit.apps.TaggitAppConfig',
    'taggit_templatetags2',
    'photo.apps.PhotoConfig',
]

1. DB설계

👉 Album 테이블과 Photo 테이블 두 개가 필요하다.
두 테이블 간에는 1:N관계가 성립한다. Album은 여러개의 Photo를 가질 수 있고, Photo는 하나의 Album에만 속할 수 있다. 이것은 ForeignKey 필드를 통해 설정할 수 있다. 외래키라고도 한다.

photo/models.py

from django.db import models
from django.urls import reverse
from photo.fields import ThumbnailImageField

class Album(models.Model):
    name = models.CharField(max_length=30)
    description = models.CharField('One Line Description', max_length=100, blank=True)

    class Meta:
        ordering = ('name',)
    
    def __str__(self):
        return self.name

    #이 메소드에서는 들어온 객체를 지칭하는 url을 반환.
    def get_absolute_url(self):
        return reverse('photo:album_detail', args=(self.id,))
    
class Photo(models.Model):
    album = models.ForeignKey(Album, on_delete=models.CASCADE) #소속된 앨범객체를 가리킴
    title = models.CharField('TITLE', max_length=30)
    description = models.TextField('Photo Description', blank=True)
    image = ThumbnailImageField(upload_to='photo/%y/%m') #upload_to옵션으로 저장할 위치를 지정. MEDIA_ROOT의 하위에 /photo/2019/08이라는 디렉터리를 만들고 사진을 저장함.
    upload_dt = medels.DateTimeField('Upload Date', auto_now_add=True) #auto_now_add=True옵션으로 현재시각을 자동저장.

    class Meta:
        ordering = ('title',)
    
    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('photo:photo_detail', args=(self.id,))#들어온 객체를 지칭하는 url을 반환

photo/admin.py

from django.contrib import admin
from django.models import Album, Photo

#앨범객체를 보여줄 때 객체에 연결된 사진객체를 같이 보여주기 위해서 세로로 나열되게 지정(TabularInline은 행으로 나열)
class PhotoInline(admin.StackedInline):
    model = Photo
    extra = 2 #추가로 입력할 수 있는 Photo 테이블 객체 수는 2 개.

@admin.register(Album)#데코레이터 장점 : 간편함.
class AlbumAdmin(admin.ModelAdmin):
    inlines = (PhotoInline,)#앨범 객체 수정 화면을 보여줄 때 PhotoInline에서 정의한 사항을 보여줌.
    list_display = ('id', 'name', 'description')

@admin.register(Photo)
class PhotoAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'upload_dt')

1.5 커스텀필드 만들기

👉 커스텀필드(custom-model-fields)
앞서 사용한 TaggableManager와 지금 사용한 ThumbnailImageField 모두 커스텀 필드이다. 장고에서 기본적으로 제공하는 필드가 아니라 사용자가 직접 만든 필드를 의미한다. TaggableManager필드는 taggit패키지에 포함되어 있어서 그대로 사용한 것이지만 ThumbnailImageField는 패키지에 포함된 것이 아니어서 직접 만들어 사용할 것이다.

import os
from PIL import Image #python이 제공하는 이미지처리 라이브러리 PIL.Image 임포트
from django.db.models.fields.files import ImageField, ImageFieldFile #기존의 ImageField클래스, ImageFieldFile클래스를 상속받음. 

class ThumbnailImageFieldFile(ImageFieldFile): #ImageFieldFile 상속받음
    def _add_thumb(s):
        parts = s.split(".")
        parts.insert(-1, "thumb")
        if parts[-1].lower() not in ['jpeg', 'jpg']:
            parts[-1] = 'jpg'
        return ".".join(parts)

    #@property를 사용해서 메서드를 멤버변수처럼 사용할 수 있음.
    #웜본 파일의 경로 path속성에 썸네일의 경로 thumb_path를 추가할 수 있음.
    @property
    def thumb_path(self):
        return self._add_thumb(self.path)

    #웜본 파일의 URL인 url속성에 썸네일의 URL인 thumb_url을 추가할 수 있음.
    @property
    def thumb_url(self):
        return self._add_thumb(self.url)

    def save(self, name, content, save=True): #파일시스템에 파일을 저장하고 생성하는 메서드
        super().save(name, content, save) #부모 ImageFieldFile클래스의 save()메서드를 호출해서 원본 이미지를 저장.

        img = Image.open(self.path)
        size = (self.field.thumb_width, self.field.thumb_height)
        img.thumbnail(size) #PIL 라이브러리의 썸네일 만드는 함수임. Image.thumbnail()이라는 함수를 이용하여 썸네일 제작.
        background = Image.new('RGB', size, (255, 255, 255))
        box = (int((size[0]- img.size[0]) / 2), int((size[1] - img.size[1]) / 2))
        background.paste(img, box)
        background.save(self.thumb_path, 'JPEG') #img와 box를 함쳐 만든 최종 썸네일을 JPEG형식으로 thumb_path 경로에 저장.

    def delete(self, save=True): #원본과 썸네일을 모두 삭제.
        if os.path.exists(self.thumb_path):
            os.remove(self.thumb_path)
        super().delete(save)

class ThumbnailImageField(ImageField): #ImageField를 상속, models.py에서 사용하는 메서드.
   #새로운 FileField클래스를 정의할 때는 그에 상응하는 File처리 클래스를 attr_class 속성에 꼭 지정해줘야함.
   #line5에서 만들어둔 ThumbnailImageFieldFile 클래스로 지정.
   attr_class = ThumbnailImageFieldFile 

    def __init__(self, verbose_name=None, thumb_width=128, thumb_height=128, **kwargs): #디폴트가 128px
        self.thumb_width, self.thumb_height = thumb_width, thumb_height
        super().__init__(serbose_name, **kwargs)

👩‍💻 썸네일을 지정하기 위해서 커스텀 필드를 만들어보았다.
하지만, 썸네일이 필요하다고 해서 꼭 이렇게 필드를 만들어야하는 것은 아니다. 장고에서는 image-kit, sorl-thumbnail, easy-thumbnails 같은 유용한 패키지들을 제공하고 있기 때문에 사실 이런 패키지를 쓰는 것이 훨씬 효율적일 수 있다.
다만, 공부를 위해서 만들어보았는데... 다음부터는 그냥 패키지를 설치해서 쓰는게 좋을 것 같다. 그게 장고의 큰 장점이기도 하니까.(✿◕‿◕✿) 너무 힘들었다....😩 에러잡는데만 반나절이 걸렸다(ㄒoㄒ)


1.7 DB 설정 확인하기


Album/Photo테이블이 잘 만들어졌다.

PhotoInline에서 설정해놓은 대로 앨범에 포토를 세로로 두 개 씩 등록할 수 있다.

1:N관계의 앨범/포토 테이블이 잘 설정된 것을 확인할 수 있다.


2. urls.py 코딩

'mysite/urls.py'

from django.conf.urls.static import static
from django.conf import settings

static()함수 임포트 -> 정적 파일을 처리하기 위해 그에 맞는 URL패턴을 반환하는 함수
setting 변수 임포트 -> settings.py 모듈에서 정의한 항목들을 담고 있는 객체를 가리킴

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', HomeView.as_view(), name='home'),
    path('bookmark/', include('bookmark.urls')),
    path('blog/', include('blog.urls')),
    path('photo/', include('photo.urls')),

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

/photo/url요청이 들어올 경우 photo/urls.py로 연결해준다.

🙄더 알아보기
static() 함수 / static(prefix, view=django.views.static.serve, **kwargs)
settings.MEDIA_URL로 정의된 /media/에 URL요청이 오면 django.views.static.serve() 뷰 함수가 처리하고, 이 뷰 함수에 document_root = settings.MEDIA_ROOT 키워드 인자가 전달된다. static.serve()함수는 개발용이고 상용에는 웹서버 프로그램을 사용한다.(출처:파이썬 웹 프로그래밍)

photo/urls.py

from django.urls import path
from photo import views

app_name='photo'
urlpattterns = [
    #/photo/
    path('', views.AlbumLV.as_view(), name='index'),
    #/photo/album/ (same as /photo/)
    path('album', views.AlbumLV.as_veiw(), name='album_list'),
    #/photo/album/99/
    path('album/<int:pk>/', view.AlbumDV.as_view(), name='album_detail'),
    #/photo/photo/99/
    path('photo/<int:pk>/', views.PhotoDV.as_view(), name='photo_detail'),
]

3. views.py 코딩

from django.views.generic import ListView, DetailView
from photo.models import Album, Photo

class AlbumLV(ListView):
    model = Album

class AlbumDV(DetailView):
    model = Album

class PhotoDV(DetailView):
    model = Photo

해당 모델 클래스만 지정해주면 되기 때문에 아주아주 간단한 뷰를 완성했다. 이렇게 간단한 뷰의 경우 urls.py에서 뷰를 정의하기도 한다.
ex)

ListView.as_view(model=Album)

4. templates 코딩

템플릿 명을 따로 지정하지 않았기 때문에 상속받은 제네릭 뷰의 디폴트 템플릿명인 album_list.html, album_detail.html, photo_detail.html으로 파일명을 지정해줘야 한다.

album_list.html

{% extends "base.html" %}

{% block title %}album_list.html{% endblock %}

<!-- {% block extra-style %}
{% endblock %} -->

{% block content %}

    {% for item in object_list %} <!-- object_list는 AlbumLV 클래스형 뷰에서 넘겨주는 컨텍스트 변수로 Album리스트가 담겨있음-->
    <div class="mt-5">
        <a class="h2" href="{% url 'photo:album_detail' item.id %}">{{ item.name }}</a>&emsp; <!--ex) /photo/album/99/-->
        <span class="font-italic h5">>{{ item.description }}</span>
    </div>

    <hr style="margin: 0 0 20px 0;">

    <div class="row">
        {% for photo in item.photo_set.all|slice:":5" %} <!-- python문법과 같음. 0~4까지 가져옴-->
        <div class="ml-5">
            <div class="thumbnail">
                <a href="{{ photo.get_absolute_url }}">
                    <img src="{{ photo.image.thumb_url }}" style="width: 100%;">
                </a>
            </div>
        </div>
        {% endfor %}
    </div>
    {% endfor %}
{% endblock %}

album_detail.html

{% extends "base.html" %}

{% block title %}album_detail.html{% endblock %}

<!-- {% block extra-style %}
{% endblock %} -->

{% block content %}
<div class="mt-5">
    <span class="h2">{{ object.name }}&emsp;</span>
    <span class="h5 font-italic">{{ object.description }}</span>
</div>

<hr style="margin: 0 0 20px 0;">

<div class="row">
    {% for photo in object.photo_set.all %}
    <div class="col-md-3 mb-5">
        <div class="thumbnail">
            <a href="{{ photo.get_absolute_url }}">
                <img src="{{ photo.image.thumb_url }}" style="width:100%;">
            </a>
        </div>
        <ul>
            <li class="font-italic">{{ photo.title }}</li>
            <li class="font-italic">{{ photo.upload_dt|date:"Y-m-d" }}</li>
        </ul>
    </div>
    {% endfor %}
</div>
{% endblock %}

photo_detail.html

{% extends "base.html" %}

{% block title %}album_list.html{% endblock %}

<!-- {% block extra-style %}
{% endblock %} -->

{% block content %}

<h2 class="mt-5">{{ object.title }}</h2>

<div class="row">
    <div class="col-md-9">
        <a href="{{ object.image.url }}">
            <img src="{{ object.image.url }}" style="width: 100%;">
        </a>
    </div>
    <ul class="col-md-3 mt-3">
        <li class="h5">Photo Description</li>
            {% if object.jescription %}
                <p>{{ object.description|linebreaks }}</p>
            {% else %}
                <p>(blank)</p>
            {% endif %}
        <li class="h5">Date Uploaded</li>
            <p class="font-italic">{{ object.upload_dt }}</p>
        <li class="h5">Album Name</li>
            <p class="font-italic">
                <a href="{% url 'photo:album_detail' object.album.id %}">{{ object.album.name }}</a>
            </p>
    </ul>
</div>
{% endblock %}

😯 만약 이런 에러가 발생한다면...

raise ImproperlyConfigured(msg.format(name=self.urlconf_name))
django.core.exceptions.ImproperlyConfigured: The included URLconf '<module 'photo.urls' from 'C:\\Users\\hjs05\\Desktop\\Django_web_project\\pyDjango\\ch99\\photo\\urls.py'>' does not appear to have any patterns in it. If you see valid patterns in the file then the issue is probably caused by a circular import.

한참 원인을 찾던 중 정말 말도안되는 오타로 문제를 해결했다.ㅠㅠ
urlpattterns.......라고 쓴 것.


✍ 마치며...

오늘은 필드를 직접 만들어 본 점이 제일 공부가 많이 된 것 같다. 하지만 만들어 본 결과 장고 패키지를 열심히 찾아서 적절한 패키지를 이용해 만드는게 더 좋지 않을까 하는 생각이 들었다. 하지만 알아놓으면 훗날 써먹는 날이 오지 않을까...?

한컴타자연습을 해야하나 오타 정말 싫다ㅠㅡㅠ
마지막에 갑자기 알 수 없는 에러가 생겼는데 module object is not itarable 이라고 에러문구가 뜨는 바람에 for문을 샅샅히 살펴봤다. 하지만 문제를 찾을 수 없어서 stackoverflow에서 관련 에러를 열심히 찾아본 결과 urls의 문제라는 것을 알고 mysite/urls.py, photo/urls.py를 샅샅히!! 정말 코드 한 줄 한 줄 살펴보면서 에러를 찾았는데!! 아무것도 찾을 수 없었다!!!!!!!
코드를 세번 쯤 읽어보던 중 갑자기 urlpattterns라는 오타가 보였다.
~( TロT)σ 세상에서 제일 슬픈 이모티콘을 넣어야하는 상황...

profile
https://github.com/Jeongseo21

0개의 댓글