본문 바로가기
django

2021.11.11 Django Multi Image

by 해맑은 코린이 2021. 11. 11.

2021.11.11 Django Multi Image_정리노트

 

 

와!!!! 정말 오랜만의 포스팅 ㅎㅅㅎ 

 

그동안 노마드코더 바닐라 JS 챌린지도 졸업하고, 이리저리 풀고 싶은 썰이 너무 많아서 드릉드릉 ...ㅎ..사실 근데 별거 안하긴했지만 그건 회고록 ? 이나 따로 사담으로 두고 2달만의 포스팅! 게다가 장고라니!!!! 너어무 반가워~~~~~

나도 정말 오랜만에 장고 포스팅 올리는듯 ㅎㅅㅎ 그럼 가봅시당

 

pip install Pillow

 

이미지 필드는 Pillow 깔아야함 ㅇㅇ 

 

이미지 기본 root ,url 설정 해줍시다

 

 

 

settings.py

media, static root 설정! DIRS 은 아직 설정해주지 않았다.

 

 

urls.py

미디어 파일 저장되는 경로도 urls.py 에 설정해쥬규 기본 이미지 세팅은 끝!

추가로 찾아본 결과, 장고는 자체적으로 저장해주지 않기 때문에, 따로 settings 에 있는 MEDIA_ROOT 경로대로 저장해주세요~ 라고 따로 세팅한거고, settings 에서 DEBUG =True 일때만 동작하고, false 라면 빈 리스트를 return 하게 된다고 하네욤! 기본으로 true 옵션이니께 그냥 요롷게 써주면 우리가 써준 경로로 이미지는 잘 저장되겠균

 

 

혹시 잘모른다면,

https://korinkorin.tistory.com/9

 

2020.08.27 _Django_ Static , Media _정리

django -- STATIC_URL , STATIC_ROOT , STATICFILES_DIR, Media file 정리 노트 이 포스팅에서 korinkorin.tistory.com/3 2020.08.20 django_CRUD_1 장고 첫 노트! 그래서 장고에서 프로젝트를 시작하는 방법부터..

korinkorin.tistory.com

 

무려.. 1년보다 더 되었지만 이 게시물 참고!

 

 

이제 모델작성하러 가볼까 멀티이미지를 위해서는 모델을 2가지를 쓴다. 하나는 글을 작성하는 Post, 하나는 이미지들을 저장하는 Images!

 

models.py

from django.db import models
from django.contrib.auth.models import User
from django.template.defaultfilters import slugify


class Post(models.Model):
    user = models.ForeignKey(User,on_delete=models.CASCADE, related_name='post')
    title= models.CharField(max_length=10)
    content = models.TextField()
    # object 를 Post 의 title 문자열로 반환
    def __str__(self):
        return self.title


# image file naming
# instance => 현재 정의된 모델의 인스턴스 
# filename => 파일에 원래 제공된 파일 이름. 이것은 최종 목적지 경로를 결정할 때 고려되거나 고려되지 않을 수 있음
def get_image_filename(instance, filename):
    # 해당 Post 모델의 title 을 가져옴
    title = instance.post.title
    # slug - 의미있는 url 사용을 위한 필드.
    # slugfy 를 사용해서 title을 slug 시켜줌.
    slug = slugify(title)
    # 제목 - 슬러그된 파일이름 형태
    return "post_images/%s-%s" % (slug, filename)  



# default, upload_to 먼지 보기
class Images(models.Model):
    # default = None 으로 이미지를 등록하지 않을 때는 db에 저장되지 않음.
    post = models.ForeignKey(Post, default=None,on_delete=models.CASCADE, related_name="image")
    # get_image_filename method 경로 사용
    # 문자열로 경로를 지정할 경우, media/문자열 지정 경로로 저장되며, 중간 디렉토리 경로를 지정할 수 있고,
    # 메소드(함수)로 지정할 경우, 중간 디렉토리 경로명뿐만 아니라 파일명까지 지정 가능
    image = models.ImageField(upload_to=get_image_filename)
    # admin 에서 모델이름
    class Meta:
        # 단수
        verbose_name = 'Image'
        # 복수
        verbose_name_plural = 'Images'

    # 이것도 역시 post title 로 반환
    def __str__(self):
       return str(self.post)

그으냥 넘어갈 내가 아니지 

 

 

ImageField upload_to 옵션에 대해서 찾아봤움.

 

  image = models.ImageField(upload_to=get_image_filename)

 

장고에서는 파일을 다룰 때 API 를 제공하는데, 

출처 장고 공식 문서 - https://docs.djangoproject.com/ko/3.2/topics/files/

 

여기서 우리가 아까 설정해준 MEDIA_ROOT 로 파일이 저장될 때 어떤 형식으로 저장해줄지 정의하는 것이 upload_to  옵션이라고 하는군뇨

 

 

오옹 FileField 또는 ImageField 에서 어떻게 쓰는지 하나 더 들어가보자.

 

출처 -장고 공식문서

 

여기서 upload_to 옵션은 문자열이 될수도 있고, 함수도 쓸 수 있다고 설명을 하네 함수로 쓰면 중간 디렉토리 경로뿐만 아니라 파일 이름도 이쁘게 쓸 수 있다고 하니 함수를 따로 적어봅시단

 

# image file naming
# instance => 현재 정의된 모델의 인스턴스 
# filename => 파일에 원래 제공된 파일 이름. 이것은 최종 목적지 경로를 결정할 때 고려되거나 고려되지 않을 수 있음


def get_image_filename(instance, filename):
    # 해당 Post 모델의 title 을 가져옴
    title = instance.post.title
    # slugfy 를 사용해서 title을 slug 시켜줌.
    slug = slugify(title)
    # 제목 - 슬러그된 파일이름 형태
    return "post_images/%s-%s" % (slug, filename)

두 가지 인수 instance 와 filename 을 받고, slugfy 를 사용해서 의미있는 url 로 사용될 수 있게 해주었다.

 

slug

URL 의 구성 요소. 웹사이트의 특정 페이지를 가리킬 때 읽기 쉬운 형식의 식별자로 사용.

장고에서는 SlugFiled 를 지원. 이를 사용하면 가독성을 높일 수 있음.,

공백은 _ 로 , 영어를 사용한다면, 대문자는 소문자로 치환된다. 

 

장점

사람이 이해하기 좋다. (/1/ 보다 /blog/ 가 좋다)

제목과 URL을 동일하게 맞춰 검색엔진 최적화(SEO)에 도움이 된다.

 

출처 - 장고 공식문서

 

공식문서 최고 -! 여기서는 이미지파일을 slug 형태로 반환하여

 

이렇게 Post 제목 - 공백은 _ 로 잘 저장되는 것을 볼 수 있다. 음음.. 관련 설명 자료가 너무 없어서 결국 공식문서로 갔는데 오랜만에 장고해서 그런가.. 친절한 공식 문서에 너무 감동받았다..흑흑

 

 # default = None 으로 이미지를 등록하지 않을 때는 db에 저장되지 않음.
    post = models.ForeignKey(Post, default=None,on_delete=models.CASCADE, related_name="image")

 

F.K 옵션은 이미지를 등록하지 않을 때 디폴트로 db 에 저장되지 않는 default= None 옵션.

그 다음에 related_name 옵션으로 해당 데이터들을 접근할 때의 이름을 설정.

 

https://fabl1106.github.io/django/2019/05/27/Django-26.-%EC%9E%A5%EA%B3%A0-related_name-%EC%84%A4%EC%A0%95%EB%B0%A9%EB%B2%95.html

 

Django 26. 장고 related_name 설정방법

장고 프로젝트에서 related_name에 대해서 설정하는 방법에 대한 내용입니다. 장고 related_name 설정방법, 장고 related_name, 장고 related_name설정하기

fabl1106.github.io

그냥 영어 뜻 그대로 나는 당연히 post 에 연관된 이름이니까 post 겠지 ?!? 했는데 정말 잘못 생각하고 있었균. 

 

예전에 댓글 게시물 때 잠시 언급했었던거 같은데 , F.K 로 역참조를 할 때 post.comment_set.all 기본 값으로 접근하기보다는 related_name 을 써서 post.related_name.all 로 데이터에 접근할 수 있었다고.. 포스팅했는데 그 때는 역참조가 뭔지도 몰라서.. 그냥 접근할 수 있다고만 포스팅했기 때문에 굳이 불러오진 않겠다 ㅎㅅㅎ 

 

역참조는 일반적으로 여기서 보면, 이미지파일에서 포린키로 연결된 포스트를 가져오는게 정참조고, 포스트 파일에서 참조된 이미지 파일을 불러오는 것은 역참조겠지!

 

 

출처 - 내 블로그 ㅎ

 

뭔가 장고는 예전에 적어놓은게 많아서 하나하나 포스팅했는지 참고하게 되네 ㅎㅅㅎ 이거 말고도 포린키라는 관계형 데이터베이스도 저장해놓은 글 있는데 그거슨 또 찾아보시면 재밌을거임 *^^*

 

 

끌끌 모델하나 끝냈는데 진짜 오랜만에 하나하나 다 찾아보면서 포스팅하려니 정말 쉽지않쿤.... 역시 장고도 공부할 거 깡깡 멀었다.

 

 

자!!!! 모델을 생성해주었으면!!!!!

 

pyton manage.py makemigrations

python manage.py migrate

 

마이그레이션 마이그레이트 음음. 데이터베이스에 우리 모델들을 적용시키는 명령어다.

 

 

 

verbose_name 으로 적어준 모델 이름들 어드민 들어가면 잘 보여욘

 

 

 

 

self.title, self.post 로 설정해준 오브젝트 이름 또한 잘 보여욘 홀홀

 

어드민에서 보기 위해서는

 

admin.py

from django.contrib import admin
from .models import *


admin.site.register(Post)
admin.site.register(Images)

에서 설정해주면 되겠지

 

오랜만에 포스팅하니까 뭔가 종합본 같자너 ㅋㅅㅋ

 

 

이제 모델기반 모델폼을 위해서 forms.py 를 앱경로에 만들어주고, 모델폼 작성!

 

 

forms.py

from django import forms
from .models import *
from django.utils.translation import gettext_lazy as _


class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'content', )
        # form 이름 지정
        labels = {
            'title': _('Title'),
            'content':_('Content')
        }
        # 제목이 10자가 넘어가면 에러메세지를 띄워줌. 
        error_messages = {
            'Title': {
                'max_length': _("제목은 10자이내로 가능합니다."),
            },
        }
 
 
class ImageForm(forms.ModelForm):
    class Meta:
        model = Images
        fields = ('image', )
        labels = {
            'image': _('Image'),
            
        }

 

역시 모델을 기반으로한 modelform 생성! 모델에서 양식을 지정해주었으니까 forms 에서 양식을 지정해주면 중복되니, 장고에서 제공하는 class Meta 라는 도우미 클래스를 써줍시당

 

label 은 나중에 html 에서 띄울 때 해당 필드의 이름 정의! 사실 지금은 라벨을 따로 정의해주지 않아도 첫번째 글자가 대문자로 해서 자동으로 띄워주긴 하지만, 이렇게 써서 이름을 지어줄 수 있다고 보여주고 싶었음 ㅎㅅㅎ

 

 

 

후.. 이제 view 로 가자!

 

 

views.py

from django.shortcuts import render
from django.shortcuts import render
from django.forms import modelformset_factory
from django.http import HttpResponseRedirect
# form,model 전부 가져오기
from .forms import *
from .models import *



def index(request):
    all_post = Post.objects.all()
    return render(request,'index.html',{'all_post':all_post})

def post(request):
    # 하나의 modelform 을 여러번 쓸 수 있음. 모델, 모델폼, 몇 개의 폼을 띄울건지 갯수 
    ImageFormSet = modelformset_factory(Images,form=ImageForm, extra=3)
   
    if request.method == 'POST':
        
        postForm = PostForm(request.POST)
        # queryset 을 none 으로 정의해서 이미지가 없어도 되도록 설정. none 은 빈 쿼리셋 리턴
        formset = ImageFormSet(request.POST, request.FILES,
                               queryset=Images.objects.none())
    
        # 두 모델폼의 유효성 검사를 해주고
        if postForm.is_valid() and formset.is_valid():
            # 저장을 잠시 멈추고
            post_form = postForm.save(commit=False)
            # Post user 에 현재 요청된 user 를 담아서 
            post_form.user = request.user
            # 저장. 이 작업 안하면 user null error
            post_form.save()
            # 유효성 검사가 왼료된 formset 정리된 데이터 모음
            for form in formset.cleaned_data:
               
                if form:
                    # image file 
                    image = form['image']
                    print(form)
                    print(form['image'])
                    # post, image file save
                    photo = Images(post=post_form, image=image)
                    photo.save()
            # index url 로 요청보냄
            return HttpResponseRedirect("/")
        # 유효성 검사 실패시 에러메세지를 터미널상에 print
        else:
            print(postForm.errors, formset.errors)
    else:
        # POST 요청이 아닌 경우 
        postForm = PostForm()
        formset = ImageFormSet(queryset=Images.objects.none())
    
    return render(request, 'create.html',
                  {'postForm': postForm, 'formset': formset})

 

휴.. 길다 끊어서 하나하나 봅시다요

 

 # 하나의 modelform 을 여러번 쓸 수 있음. 모델, 모델폼, 몇 개의 폼을 띄울건지 갯수 
    ImageFormSet = modelformset_factory(Images,form=ImageForm, extra=3)

요 코드는 장고에서 역시 제공하는 formset 기능으로, 우리는 지금 이미지를 여러개 불러오고 싶으니까 여러개 쓸 모델, 모델폼, 또 3개로 해당 폼을 띄울 갯수를 정해주었다.

 

    if request.method == 'POST':
        
        postForm = PostForm(request.POST)
        # queryset 을 none 으로 정의해서 이미지가 없어도 되도록 설정. none 은 빈 쿼리셋 리턴
        formset = ImageFormSet(request.POST, request.FILES,
                               queryset=Images.objects.none())
    
        # 두 모델폼의 유효성 검사를 해주고
        if postForm.is_valid() and formset.is_valid():
            # 저장을 잠시 멈추고
            post_form = postForm.save(commit=False)
            # Post user 에 현재 요청된 user 를 담아서 
            post_form.user = request.user
            # 저장. 이 작업 안하면 user null error
            post_form.save()
            # 유효성 검사가 왼료된 formset 정리된 데이터 모음

이 부분은 댓글때도 했던 것처럼 POST 요청이 들어오면 postForm, Image formset 에 요청과 Image 는 request.FILES 까지 넘겨주는데, 이 때 이미지가 우리는 하나도 없어도 유효성 검사를 통과해야하므로 .none() 으로 설정해준 모습!

 

또 둘다 유효성 검사를 통과하면, Post Form 에서는 user null error 를 막기 위해 저장을 잠시 멈추고 현재 요청된 user (현재 로그인된 user ) 를 담아서 저장을 해준다!

 

 

이제 이미지도 저장해줘야지

 

# 유효성 검사가 왼료된 formset 정리된 데이터 모음
            for form in formset.cleaned_data:
               
                if form:
                    # image file 
                    image = form['image']
                    # F.K post, image file save
                    photo = Images(post=post_form, image=image)
                    photo.save()
            # index url 로 return
            return HttpResponseRedirect("/")

 

오잉 cleaned_data 가 또 왜 갑자기 나왔냐해스 보니까 

 

출처 - 장고 공식 문서

아아아 오키오키 그니까 유효성 검사를 통과한 데이터만 깔끔쓰하게 정리해서 보내주는거균

 

 

터미널에 찍어보니까 이렇게 정리되어서 왔네 ㅇㅇ

그래서 이미지 또한 이렇게 저장을 완료하고! 단순히 그냥 url 만 보내주면 되니까 HttpResponseRedirect 로 인덱스로 보내주었음!

 

 

 

휴.... 이제 템플릿에서 띄워주는건 어케하남

 

templates/create.html

 

<form id="post_form" method="post" action="" enctype="multipart/form-data">
 
    {% csrf_token %}

    <b>{{postForm.title.label}}</b><br />
    {{postForm.title}}<br />


   <b> {{postForm.content.label}}</b> <br />
   {{postForm.content}}<br />


    {{ formset.management_form }}
    {% for form in formset %}
        {{ form }}<br />
    {% endfor %}
 
 
    <input type="submit" name="submit" value="Submit" />
</form>

 

postForm 은 그냥 원래썼던것처럼 {{postForm.as_p}} 로 써도 되고 나처럼 label 이랑 필드를 각각 써서 나타내도 됩니다롱

 {{postForm.as_p}} 이렇게 해주면 각각 라벨과 필드에 p태그로 묶임!

 

 

 

이미지는 formset 이기 때문에 따로 공식문서에서 말한 것 그대로 해보져 머

 

출처 - 공식문서

 

두번째랑 똑같이 써주었다! 맨 위는 3개의 이미폼이 뭉텅이로 나오고 나같은 경우는 for 문을 통해 하나하나 따로 그냥 분리해준거 그 차이!

다만 두번째 세번째 방법은 management_form 이라는 양식을 렌더링해야하는데, 얘는 그냥 

 

출처 - 장고 공식 문서

 

유효한 데이터를 넘기기 위해 써주는 하나의 양식이라네여 ㅇㅇ!

 

 

templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <a href="{% url 'create' %}">Create Post</a><br />

    {% for post in all_post %}
        {{post.title}}<br />
        {{post.content}}<br />
        {% if post.image %}
            {% for i in post.image.all %}
            <img src="{{i.image.url}}" /><br />

            {% endfor %}
        <hr />
        {% endif %}
    {% endfor %}
 
</body>
</html>

얘는 그냥 전체 post 오브젝트들을 들고와서 for 문을 돌려줄건데, 여기서 아까 내가 말한 related_name 을 사용해서 post.image_set.all 이 아니라 post.image.all 로 들고 올 수 있는 것! 모두 들고와서 for 문으로 역시 하나하나 분리.

그리고 if.post.image 를 통해서 이미지가 있는 경우 없는 경우를 나눠주면 좀 더 확실히 나눌 수 있겠쥬.

 

 

마지막으로 image.url 부분만 정리..! 휴

 

그냥 image 로만 불러오면 db에서 문자열로 저장되기 때문에 이미지파일을 띄워줄 수 없기 때문에 image.url 로 띄워준건데, 얘는 media 폴더로 부터 시작하는 상대경로의 파일을 띄워준다. image.path 는 절대 경로로 가져와서 이미지 파일을 띄움! url, path 둘 중 원하는대로써주기.

 

 

대장정의 끝..! url 만 연결해주고 끝내자!!!!!!!

 

urls.py

from django.contrib import admin
from django.urls import path
from CRUD.views import *
from django.conf.urls.static import static
from django.conf import settings


urlpatterns = [
    path('admin/', admin.site.urls),
    path('',index,name="index"),
    path('create/',post, name="create"),
]+ static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT)

 

 

 

실행 화면 

http://127.0.0.1:8000/create/

http://127.0.0.1:8000/

 

 

 

 

 

 

 

와!!!! 성공. css 로 나머지는 만져주면 될 듯 하다...ㅎㅅㅎ.. for문으로 하나하나 태그를 달아주었으니 나머지는 css 만 적용해서 하면 그럴듯하게 나올듯!!!!

 

 

 

 

 


와 진짜 간만의 포스팅이야 반갑균..... 그리고 오랜만에 해서 그런가 하나하나 장고의 공식문서를 보며 감탄..또 감탄... 그리고 정말 여러 기능들이 생각보다 많아서 역시 장고는 참 잘만든 프레임워크인거 같아 애정이 생겨따 ㅎㅅㅎ 흐흐 쨋든 오늘의 포스팅 끝끝!!!!!

댓글