본문 바로가기

Django

[Django] Photo 앱 개발

지금 만들 포토 앱은 사진들을 앨범으로 그룹화해 관리하고 각 사진에 대한 정보를 등록하고 열람할 수 있는 앱이다.

 

포토앱에 필요한 테이블은 사진을 담는 Photo 테이블과

사진들을 그룹화해 정보를 담을 수 있는 Album 테이블이 필요하다.

또한 사진을 사이트에 등록하는 업로드 기능 및 썸네일 사진을 생성하는 기능도 필요하다.

 

테이블 설계

Album 테이블과 Photo 테이블 2개가 필요하다. 두 테이블 간에는 1:N 관계가 성립된다.

즉 하나의 앨범은 여러 개의 사진을 가질 수 있고, 하나의 사진은 하나의 앨범에만 속하는  관계이다.

이 관계는 Photo 테이블의 album 속성에 ForeignKey 필드로 지정된다.

 

setting.py 에 다음과 같은 블로그 앱 등록을 해준다.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # shkim
    'taggit.apps.TaggitAppConfig',
    'taggit_templatetags2',

    'bookmark.apps.BookmarkConfig',
    'blog.apps.BlogConfig',
    'photo.apps.PhotoConfig',
]

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

 

models.py 수정

from django.db import models
from django.urls import reverse

from photo.fields import ThumbnailImageField


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

    class Meta:
        ordering = ('name',)

    def __str__(self):
        return self.name

    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('IMAGE', upload_to='photo/%Y/%m')
    upload_dt = models.DateTimeField('UPLOAD DATE', 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,))

import해준 ThumbnailmageField는 사진에 대한 원본 이미지와 썸네일 이미지를 모두 저장할 수 있는 필도로

직접 커스텀한 필드이다. (이 커스텀 필드는 뒷부분에 정의하겠다)

- get_absolute_url() : 메소드가 정의된 객체를 지칭하는 URL을 반환한다. 메소드 내에서는 장고의 내장 함수인 reverse()를 호출한다. 위 코드에서는 /photo/album/99/ 형식의 URL을 반환한다.

-album 컬럼은 Album 테이블에 연결된 외래 키이다. 사진이 소속된 앨범 객체를 가리키는 reference 역할을 한다.

 

admins.py 수정

from django.contrib import admin

from photo.models import Album, Photo


class PhotoInline(admin.StackedInline):
    model = Photo
    extra = 2


@admin.register(Album)
class AlbumAdmin(admin.ModelAdmin):
    inlines = (PhotoInline,)
    list_display = ('id', 'name', 'description')


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

외래키로 연결된 Album, Photo 테이블 간에는 1:N 관계가 성립되므로, 앨범 객체를 보여줄 때 객체에 연결된 사진 객체들을 같이 보여줄 수 있다. 같이 보여주는 형식은 StackedInline과 TabularInline 두가지가 있다. STackedInline은 세로로 나열되는 형식을 보여준다.

 

fileds.py

import os
from PIL import Image
from django.db.models import ImageField
from django.db.models.fields.files import ImageFieldFile


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

    @property
    def thumb_path(self):
        return self._add_thumb(self.path)

    @property
    def thumb_url(self):
        return self._add_thumb(self.url)

    def save(self, name, content, save=True):
        super().save(name, content, save)

        img = Image.open(self.path)
        size = (self.field.thumb_width, self.field.thumb_height)
        img.thumbnail(size)
        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')

    def delete(self, save=True):
        if os.path.exists(self.thumb_path):
            os.remove(self.thumb_path)
        super().delete(save)


class ThumbnailImageField(ImageField):
    attr_class = ThumbnailImageFieldFile

    def __init__(self, verbose_name=None, thumb_width=128, thumb_height=128, **kwargs):
        self.thumb_width, self.thumb_height = thumb_width, thumb_height
        super().__init__(verbose_name, **kwargs)

 

 

urls.py 수정

# mysite/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

from mysite.views import HomeView


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

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

/photo/URL 요청이 오면, 포토 앱의 APP_URLCONF에 처리를 위임한다.

기존 url 패턴에 static() 함수가 반환하는 URL 패턴을 추가한다. 

static() 함수의 형식은 다음과 같다.

static(prefix, view=django.views.static.serve, **kwargs)

 

URL 설계 내용에 따라 photo/urls.py 파일에 4개의 URL을 추가한다

from django.urls import path

from photo import views


app_name = 'photo'
urlpatterns = [

    # Example: /photo/
    path('', views.AlbumLV.as_view(), name='index'),

    # Example: /photo/album/, same as /photo/
    path('album', views.AlbumLV.as_view(), name='album_list'),

    # Example: /photo/album/99/
    path('album/<int:pk>/', views.AlbumDV.as_view(), name='album_detail'),

    # Example: /photo/photo/99/
    path('photo/<int:pk>/', views.PhotoDV.as_view(), name='photo_detail'),

]

views.py 수정

# photo/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

ListView, DetailView 제네릭 뷰를 상속받고, 해당 모델 클래스만 지정해주면 된다.

 

templates 코딩

# base.html
<li class="nav-item mx-1 btn btn-primary">
<a class="nav-link text-white" href="{% url 'photo:index' %}">Photo</a></li>
#album_detail.html

{% extends "base.html" %}

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

{% block extra-style %}
<style>
.thumbnail {
    border: 5px solid #ccc;
}
</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 %}
#album_list.html

{% extends "base.html" %} 

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

{% block extra-style %}
<style>
.thumbnail {
    border: 3px solid #ccc;
}
</style>
{% endblock %}

{% block content %} 

    {% for item in object_list %} 

    <div class="mt-5"> 
        <a class="h2" href="{% url 'photo:album_detail' item.id %}">
            {{ item.name }}</a>&emsp;
        <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" %} 
        <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 %}
# photo_detail.html

{% extends "base.html" %}

{% block title %}photo_detail.html{% 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.description %}<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 %}