웹프로그래밍/Django

[Django] widget (5) widget 만들어보기 - 이미지, Clearble File Input

ssung.k 2019. 8. 31. 22:39

widget (4)

 

[Django] widget (4) widget 만들어보기 - 캘린더 jQuery-UI

widget (3) [Django] widget (3) widget 만들어보기 - 자동완성 Select2 widget (2) [Django] widget (2) widget 만들어보기 - 별점 주기 rateit.js widget (1) [Django] widget (1) widget의 원리와 widget 만들..

ssungkang.tistory.com

개별적인 내용을 담고있지만 이전 포스팅과 전체적인 맥락은 이어지므로 참고하시면 도움이 되실겁니다. 현재 모델은 다음과 같습니다.

# core/models.py

from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator

class University(models.Model):
    name = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name

class Student(models.Model):
    university = models.ForeignKey(University, on_delete=models.CASCADE)
    date_birth = models.DateTimeField()
    residencce = models.CharField(max_length=200)
    photo = models.ImageField(blank=True)
    grade = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
    intro = models.TextField()

이번에는 Student 모델에 대해 photo 에 대해 다뤄보겠습니다. 기존의 input 태그의 file 필드를 보면 UI 적으로는 별로인게 사실입니다. 이미지도 확인할 수 가 없고 말이죠. 이 부분을 개선해보도록 하겠습니다.

 

 

Clearble File Input

각 field 에는 대응되는 widget이 존재합니다. file, image field 의 경우에는 기본적으로 Clearble File Input 이 사용되는데 이번에는 새로운 라이브러리를 가져오기 보다 이를 이용해서 진행하도록 하겠습니다.

 

기존에는 이미지를 선택하더라도 다음과 같이 이미지의 이름만을 보여줄 뿐 이미지가 보이지는 않습니다. 우선 이미지가 보이도록 해보도록 개선하겠습니다.

 

# core/forms.py

from django import forms
from django.urls import reverse_lazy
from .models import University, Student
from .widgets import CounterTextInput, starWidget, AutoCompleteWidget, DatePickerWidget, PreviewFileWidget

# 생략

class StudentForm(forms.ModelForm):
    class Meta:
        model = Student
        fields = '__all__'
        widgets = {
            'university': AutoCompleteWidget(ajax_url=reverse_lazy('university_list')),
            'grade': starWidget,
            'date_birth': DatePickerWidget,
            'photo': PreviewFileWidget,
        }
        

PreviewFileWidget 이라는 커스텀 widget 을 정의하여 사용하도록 하겠습니다.

 

# core/widgets.py

class PreviewFileWidget(forms.ClearableFileInput):
    template_name = "widgets/preview_file.html"

    class Media:
        js = [
            "//code.jquery.com/jquery-3.4.1.min.js",
        ]

ClearableFileInput 을 상속받고 CDN 방식으로 jQuery 만 필요합니다. 나머지 기능은 이를 통해 구현하도록 합니다.

 

<!-- templates/widgets/preview_file.html -->

{% include "admin/widgets/clearable_file_input.html" %}

{{ form.media }}

<script>
    $(function(){
        $('#{{ widget.attrs.id }}').change(function(){
            
            if (this.files[0]){
                var reader = new FileReader();
                reader.readAsDataURL(this.files[0]);
                reader.onload = function(e){
										$('#preview_{{widget.attrs.id}}').attr('src', e.target.result);
                };
                
            }
        })
    })
</script>

기본적으로 clearble_file_input.html 을 include 해주고 media 를 불러왔습니다. file input 이 변할 때마다 함수를 실행시켜줍니다. this.files 를 통해 input 에 들어간 file 들의 list 를 불러올 수 있습니다. 기본적으로 여러 사진을 추가 할 수 있기 때문에 list 형태로 존재하며 지금은 사진을 하나씩 넣고 있기 때문에 index 를 통해 접근이 가능합니다. file 을 읽을 수 있는 FileReader 객체를 만들어주고, 이 파일을 읽어줍니다. onload 를 통해서 파일을 다 읽을 때까지 기다렸다가 함수를 실행합니다.e.target.result 를 통해 파일을 64진수로 표현할 수 있으며 이를 img input 에 넣어줌으로써 사진을 볼 수 있습니다.

 

 

 

<!-- templates/widgets/preview_file.html -->

{{ form.media }}

<div class="img_cotainer" style="display: flex; align-items: center;">

    {% if widget.value %}
        {{ widget.value.url }}
    {% endif %}

    <img id="preview_{{ widget.attrs.id }}" style="width: 100px; height:100px; display: none;">

    {% include "admin/widgets/clearable_file_input.html" %} 
</div>


<script>
    $(function(){
        $('#{{ widget.attrs.id }}').change(function(){
            
            if (this.files[0]){
                var reader = new FileReader();
                reader.readAsDataURL(this.files[0]);
                reader.onload = function(e){
                    $('#preview_{{widget.attrs.id}}').attr('src', e.target.result).show();
                    
                };
                
            }
        })
    })
</script>

실습을 따라하고 계시다면 위 사진과 다른 위치에 사진이 보이실 겁니다. 사진 위치를 좀 조절하기 위해서 약간의 style 을 추가하였습니다. 처음에는 사진 공간이 보여지지 않고 사진이 들어가는 순간 show 를 통해 보여지게 됩니다. 또한 위에서의 문제는 file 의 값이 바뀌었을 때는 그 이미지를 바로 띄워주지만 저장한 후 다시 확인하러 들어가면 js 코드가 실행이 안되니 확인 할 수 없습니다. 따라서 widget.value.url 를 통해 해당 이미지를 불러옵니다.

이미지의 이름은 뜨지만 사진은 아직 뜨지 않습니다. 이에 대해서는 media 처리를 해줘야합니다. 이에 대한 자세한 설명은 media 에 대한 이전 포스팅을 참조해주세요.

media 파일 설정하기

 

[Django] media 파일 업로드하기

Media 파일이란? media 파일이란 FileField 를 통해 저장한 모든 파일을 지칭합니다. 물론 ImageField 도 FileField 를 상속받은 필드로서 유사 필드들을 통해 저장된 파일도 media 파일입니다. 이는 특별하게 db..

ssungkang.tistory.com

 

# config/settings.py

# 생략 

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# config/urls.py

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('admin/', admin.site.urls),
    path('', include('core.urls')),
]

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

다음과 같은 설정 후, MEDIA_ROOT 에. 해당하는 경로에 media 폴더를 만들어줍니다. 지금은 manage.py 와 같은 위치가 될 것 입니다.

 

<!-- templates/widgets/preview_file.html -->

{{ form.media }}

<div class="img_cotainer" style="display: flex; align-items: center;">

    {% if widget.value %}
        <img src="{{ widget.value.url }}" style="width:100px; height:100px;">
        
    {% endif %}

    <img id="preview_{{ widget.attrs.id }}" style="width: 100px; height:100px; display: none;">

    {% include "admin/widgets/clearable_file_input.html" %} 
</div>

<script>
    // 위와 동일하여 생략
</script>

결과적으로 widget.value.url 은 media 파일이 존재하는 경로가 되었으므로 위와 같이 사진을 나타낼 수 있습니다.

하지만 이럴 경우, 아래 이미지와 같은 문제가 발생합니다.

이미지가 둘 다 나오는 현상

 

<!-- templates/widgets/preview_file.html -->

{{ form.media }}

<div class="img_cotainer" style="display: flex; align-items: center;">

    {% if widget.value %}
        <img id="current_{{widget.attrs.id}}" src="{{ widget.value.url }}" style="width:100px; height:100px;">
        
    {% endif %}

    <img id="preview_{{ widget.attrs.id }}" style="width: 100px; height:100px; display: none;">

    {% include "admin/widgets/clearable_file_input.html" %} 
</div>

<script>
    $(function(){
        $('#{{ widget.attrs.id }}').change(function(){
            
            if (this.files[0]){
                var reader = new FileReader();
                reader.readAsDataURL(this.files[0]);
                reader.onload = function(e){
                    $('#preview_{{widget.attrs.id}}').attr('src', e.target.result).show();
                    $('#current_{{widget.attrs.id}}').hide()
                };
            }
        })
    })
</script>

따라서 새로운 이미지가 추가되면 기존에 있던 이미지는 hide 로 숨겨주었습니다.

아래 영상과 같이 이미지가 바뀌는걸 확인할 수 있습니다.

 

완성 시연